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
endUnscopedLocator は BaseLocator を継承しています。
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.newJob で考慮するパターン
もう一つは default_scope を頼るのではなく、Job 内で処理するべきオブジェクトかを確認するパターンです。
def perform(post)
return unless post.active?
# 処理
endどちらを採用するかは結構悩みましたが、特別なことがない限り default_scope は適用してほしいので、Locator を変更する方法で対応しました。
経緯
経緯を調べたところ、2015/08 時点では default_scope は考慮されていたようです。
2016/01 に UnscopedLocator がデフォルトの Locator になったようです。
-
Check if model responds to unscoped before locating many. · rails/globalid@2d4bc2d · GitHub
-
NoMethodError: undefined method `unscoped' for Class · Issue #82 · rails/globalid · GitHub
なんだか副作用な気もしますが... 特に Issue が作られていないところを見ると、特に困っている人はいないのか、そもそも使っている人が少ないのか...