ActiveRecord#update, ActiveRecord#save はDirtyな値を持たない時、実際のUPDATEを行わない。便利機能だけど、更新対象を複数の変数に受けている場合、ちょっと予想外の挙動をするので注意。
user = User.find(42)
user.name # => 'Tarou'
current_user = User.find(42)
current_user.name # => 'Tarou'
current_user.update!(name: nil)
user.update!(name: 'Tarou') # => Dirty でないので UPDATE 文が発行されず、 DB の name は NULL のまま
current_user.reload.name # => nil
user.reload.name # => nil
👆こんなコードを書くことは普通はないだろうけど、バッチ処理とかでいろんなデータにいろいろやるときに書いてしまうかもしれない(書いた)。例えば採番を行う処理で;
# [{ id: 1, number: 1, want_to_number: true },
# { id: 2, number: 2, want_to_number: false },
# { id: 3, number: 3, want_to_number: true }]
targets = User.where(want_to_number: true)
# 何らかの処理、ここで user のレコードが取得され、number がメモリに保持される
targets.each do |user|
user.何らかの処理
end
# number カラムのクリア
# DB の値は number = null となるが、targets の各メンバーは null となる前の number を保持したまま
# DB の値と targets の間に乖離が生じる
User.all.update_all(number: nil)
# 再採番
targets.each.with_index(1) do |user, new_number|
# このとき targets 内の id: 1 の User オブジェクトの number は 1 となっているので、
# new_number と一致し、dirty と判定されない
# したがって UPDATE 文も発行されない
user.update!(number: new_number)
end
# こうあってほしいんだけど、
# [{ id: 1, number: 1, want_to_number: true },
# { id: 3, number: 2, want_to_number: true }]
puts targets
# 実際はこうなってる
# [{ id: 1, number: nil, want_to_number: true },
# { id: 2, number: nil, want_to_number: false },
# { id: 3, number: 2, want_to_number: true }]
puts User.all
update
が Dirty なものしか UPDATE しない挙動って感覚的には何となく知ってたけど、メソッドのドキュメントには書いてないっぽい(ActiveRecord#update, ActiveRecord#save)。コードをたどっていくとActiveRecord::AttributeMethods::Dirty._update_rows
がチェックしてるのがわかった。ちなみにこの挙動は設定でオフることが出来るけど、上に示したようなバグるコードはコードが悪いので設定で対処するのはやめた方が良いと思う。
config.active_record.partial_updates = false