はじめに
callbackを利用する際に自分が予想していた挙動と異なる動きをすることがあったのでメモとして残しておきます。
問題
今回扱う問題を簡単にまとめると、「callback内で子要素をアソシエーションを用いて作成すると子要素のbefore_updateが呼ばれる」というバグになります。
作成のみするので、子要素のbefore_createだけが呼ばれるのを期待していたところ、なんとbefore_updateまで呼ばれてしまったということです。
具体的なコードを見ながら確認したほうがわかりやすいので以下に示します。
※ 例は適当です
class Point < ApplicationRecord
after_create do
point_histories.create!
end
has_many :point_histories
end
class PointHistory < ApplicationRecord
before_create do
# この部分だけ呼ばれると予想していたが...
end
before_update do
# この部分も呼ばれる
end
belongs_to :point
end
この状態で Point.create
するとその子要素であるPointHistoryクラスのbefore_updateも呼ばれます。
updateなんてどこにも書いてないのになんでbefore_updateがよばれるんだろう...って感じですね。
原因
この動きになる原因なのですが、Railsにはモデル間がアソシエーションで関連付けられている際、関連づけられているモデルを自動で保存する機能があるのでそれが悪さをしているようでした。
(そもそもcallbackではなくこれをつかってcreateすればって感じですが割愛で笑)
こういうやつですね。
point = Point.new
point.point_histories.build
point.save # pointとpoint_history両方作られる
これを今回の例にあてはめてみると、after_create内部でアソシエーションを用いて、関連づけられているモデルのレコードを作成しているのがわかります。
class Point < ApplicationRecord
after_create do
# アソシエーションを用いてレコードを作成
point_histories.create!
end
has_many :point_histories
end
アソシエーションを用いてレコードを作成しているため、上記で説明した関連づけられたモデルを自動で保存する機能も動き、(すでにレコードがあるため)before_updateが呼ばれるということになります。
解決策
callbackを利用したい場合の解決策としては「アソシエーションを用いずレコードを作成する」、「autosave: falseオプションを使用する」方法があります。
アソシエーションを用いずレコードを作成する
class Point < ApplicationRecord
after_create do
PointHistory.create!(point_id: id)
end
has_many :point_histories
end
autosave: falseオプションを使用する
class Point < ApplicationRecord
after_create do
point_histories.create!
end
has_many :point_histories, autosave: false
end
おわりに
考えてみればたしかにそうだなーという感じの挙動なのですが、意識していないとバグを生むことにもなるかもしれないのでみなさんも気をつけてください。
なお、本記事で扱ったバグはすでにRailsのissueにもあげられていますが修正には及ばずcloseされてしまったようです。