inSmartBank

B/43を運営する株式会社スマートバンクのメンバーによるブログです

ActionMailer::Baseのサブクラスで値を変更すると全てのMailerに反映されてしまう設定がある

こんにちは、サーバサイドエンジニアの@ohbaryeです。

本記事ではRailsのActionMailerで最近ハマった小ネタについて記載します。

先に結論を言うと

ApplicationMailerの設定のうち、ActionMailer::Baseのサブクラスで値を変更すると全てのMailerに反映されてしまう設定があるので気をつけましょう。

記事執筆時点*1で筆者が気づいたのはdeliver_later_queue_name, raise_delivery_errors, perform_deliveriesあたりです。

ActionMailerの設定に苦戦するプログラマのイラスト

前提: メール送信の非同期設定

Railsのメール配信機構であるActionMailerには、メール送信を非同期で送るためのdeliver_laterメソッドがあります。

このときに使用されるキューはdeliver_later_queue_nameで設定でき、使用されるJobクラスはdelivery_jobで設定できるとRails Guideで紹介されています。

この値はenvironment.rb, development.rb等のconfiguration filesで設定することができます。

Rails.application.configure do
  config.action_mailer.deliver_later_queue_name = :bulk
  config.action_mailer.delivery_job = RegistrationDeliveryJob
end

やりたかったこと: 個別のMailerクラスでキューの設定を行う

configuration filesの設定は全てのMailerに適用されるものです。使用するキューやジョブを各Mailerクラスごとに設定するにはどうすればよいでしょうか。

これら2つの値を設定するアクセサがあるので、初めは以下のような設定を思いつきました。

class UserMailer < ApplicationMailer
  self.deliver_later_queue_name = :bulk # 後述しますがこの設定は要注意!
  self.delivery_job = RegistrationDeliveryJob

  def welcome_email
    @user = params[:user]
    @url  = 'http://example.com/login'
    mail(to: @user.email, subject: '私の素敵なサイトへようこそ')
  end
end

しかしながら、この記述ではうまくいきませんでした。

この2つの設定のうちdelivery_jobはMailerクラス単位になりますが、deliver_later_queue_nameはすべてのMailerに適用されてしまいます

原因: class_attributecattr_accessorの違い

同じようなインタフェースで値を設定できるにも関わらずなぜ影響範囲が異なるのか、Railsのコードを読んでみました。

クラス単位で設定できるclass_attribute

まず、Mailerクラス個別の設定となったdelivery_jobActionMailer::Baseclass_attributeとして定義されています。これはセットしたクラスにのみ影響する変数です。

対象コード付近にあるdefault_paramsも同様です。

継承先/元で共有されるcattr_accessor

一方、deliver_later_queue_nameActionMailer::DeliveryMethodscattr_accessorとして定義されています。raise_delivery_errors, perform_deliveriesあたりの設定も同様です。*2

cattr_accessorの説明をAPIドキュメントから一部抜粋すると、子クラスで値を変更すると親クラスにも影響が及ぶもの(逆も然り)だと説明されています。

If a subclass changes the value then that would also change the value for parent class. Similarly if parent class changes the value then that would change the value of subclasses too.

これにて謎が解けました。

Rails Guideで同列に並んでいる設定なので勝手に適用範囲も同じであろうと推測・誤認してしまいました。今後は各設定の適用範囲について注意深く見ていきたいと思います。

回避策: 個別のMailerクラスでキューを指定する方法は?

キューを使い分ける方法には以下の2つがあると同僚の@nilpoonaが教えてくれました。

1) delivery_jobに与えるJobクラスの実装でqueue_asを設定する*3

class RegistrationDeliveryJob < ApplicationJob
  queue_as :bulk
  # `self.queue_name = :bulk` でも同等
end
class UserMailer < ApplicationMailer
  self.delivery_job = RegistrationDeliveryJob
end

Mailerに対応するJobクラスを作らないといけないのが冗長ですね。

2) deliver_laterのキーワード引数queueで指定する

UserMailer.with(user:).welcome_email.deliver_later(queue: :bulk)

Mailerクラス側の設定ではなく呼び出し側で決定しなければいけないのが設計として微妙に思います*4

より良い方法をご存じの方がいたらぜひ教えていただけると幸いです。


記事を書くにあたりアクセサの違いについては「Rails: クラスレベルの3つのアクセサを比較する(翻訳)」も参考にさせてもらいました。

SmartBankでは部分的にでもRailsのコードリーディングをしながら開発したいエンジニアを募集しています!

▼サーバーサイドエンジニア smartbank.co.jp

▼カジュアル面談 smartbank.co.jp

*1:rails 7.0.5を前提とします

*2:ActionMailer::DeliveryMethodsはActionMailer::Baseでincludeされています

*3:queue_asはqueue_nameをセットするクラスメソッド。queue_nameはclass_attributeなのでJobクラスごとに設定できる値。

*4:使い方を間違えると意図しない挙動をしてしまう

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.