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
UnscopedLocator
は 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.new
Job で考慮するパターン
もう一つは 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 が作られていないところを見ると、特に困っている人はいないのか、そもそも使っている人が少ないのか...