背景
こんなコードを書いた時にふとこのコードってどうなっているんだ?
となったので実際のRailsのコードを読んでみました。
やりたいことは1つのHogeに対して外部APIを必ず1回しか呼ばないようにして、その結果をFugaに保存する。
class Hoge < ActiveRecord::Base
has_oen :fuga
def piyo
return fuga if fuga.present? # <- ロック取りたくないから事前に関連があるか確認
with_lock do
reload # <- fugaのキャッシュを消したい ※1
return fuga if fuga.present?
result = call_api
create_fuga(**result)
end
end
private
def call_api
# call api
end
end
class Fuga < ActiveRecord::Base
belongs_to :hoge
end
当初※1のreloadがなくてこれってfugaがキャッシュされていてタイミングissue発生するのでは?と思ったのですが、
specでどうしてもエラーケースを再現できずRailsのコードを読んでみた。
# https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/activerecord/lib/active_record/locking/pessimistic.rb#L82-L90
# Wraps the passed block in a transaction, locking the object
# before yielding. You can pass the SQL locking clause
# as argument (see <tt>lock!</tt>).
def with_lock(lock = true)
transaction do
lock!(lock)
yield
end
end
というわけでsee lock!
# https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/activerecord/lib/active_record/locking/pessimistic.rb#L63-L80
# Obtain a row lock on this record. Reloads the record to obtain the requested
# lock. Pass an SQL locking clause to append the end of the SELECT statement
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
# the locked record.
def lock!(lock = true)
if persisted?
if has_changes_to_save?
raise(<<-MSG.squish)
Locking a record with unpersisted changes is not supported. Use
`save` to persist the changes, or `reload` to discard them
explicitly.
MSG
end
reload(lock: lock)
end
self
end
なるほど〜ん
persisted?の時には内部でreload読んでくれてるんですね。。
どうりでテスト落ちないわけですわ
というわけでreload追加する必要がないとわかってspecのケースだけ追加しましたとさ