Active Job の deserialize では default_scope は適用されない

Active Job の deserialize では、モデルの default_scope は適用されないという罠にハマったのでメモ。

バージョン情報

  • Rails v6.0.2.1
  • globalid v0.4.2

概要

例えば、以下のモデルがあるとします。 default_scope には、status:active のレコードのみ取得する条件を指定しています。

class Post < ApplicationRecord
  default_scope { where(status: :active) }
end

モデルのオブジェクトを Active Job のキューに追加します。オブジェクトは gid://foo-bar/Post/123 のような Global ID に serialize されて、キューに保存されます。

NotifyPostJob.perform_later(post)

そして、キューが実行されるまでに status:inactive など他の値が入ったものとします。

このキューが Active Job で実行されて、キューに含まれる Global ID からモデルのオブジェクトを deserialize する時に、default_scope は効きません。 僕は ActiveJob::DeserializationError が発生するものだと思っていました。

対応方法

deserialize 時に default_scope を考慮するパターンと、Job 内で考慮するパターンがあると思います。

deserialize 時に考慮するパターン

globalid gem のコードを確認すると、デフォルトでは Global ID からレコードを取得する際に UnscopedLocator という Locator が使用されます。 これは名前の通り、scope を解除してレコードを取得する処理になっています。

        def locate(gid)
          unscoped(gid.model_class) { super }
        end

        private
          # 省略

          def unscoped(model_class)
            if model_class.respond_to?(:unscoped)
              model_class.unscoped { yield }
            else
              yield
            end
          end

UnscopedLocatorBaseLocator を継承しています。

BaseLocator は、シンプルにモデルの find メソッドを呼び出す処理になっています。 この Locator では、default_scope が考慮されます。

        def locate(gid)
          gid.model_class.find gid.model_id
        end

従って、deserialize の挙動を変えるのであれば、UnscopedLocator ではなく BaseLocator を使用するように変更します。

config/initializers/globalid.rb:

# "foo-app" はアプリケーション名
GlobalID::Locator.use "foo-app", GlobalID::Locator::BaseLocator.new

Job で考慮するパターン

もう一つは default_scope を頼るのではなく、Job 内で処理するべきオブジェクトかを確認するパターンです。

  def perform(post)
    return unless post.active?

    # 処理
  end

どちらを採用するかは結構悩みましたが、特別なことがない限り default_scope は適用してほしいので、Locator を変更する方法で対応しました。

経緯

経緯を調べたところ、2015/08 時点では default_scope は考慮されていたようです。

2016/01 に UnscopedLocator がデフォルトの Locator になったようです。

なんだか副作用な気もしますが... 特に Issue が作られていないところを見ると、特に困っている人はいないのか、そもそも使っている人が少ないのか...

© 2023 暇人じゃない. All rights reserved.