before_destroy時点でdestroyされてしまう、dependentの罠
まずはこれを見て欲しい
# == Schema Information # # Table name: users # # id :integer not null, primary key # name :string(255) # class User < ActiveRecord::Base has_many :articles, dependent: :destroy after_create do articles.import(%w"1 2 3 4 5".map{|text| Article.new(text:text)}) end before_destroy do p articles.map(&:text) end end
# == Schema Information # # Table name: articles # # id :integer not null, primary key # user_id :integer # text :string(255) # class Article < ActiveRecord::Base belongs_to :user end
よくある1対多の関連を持ったモデルがありますよ、と。
さらに、dependent指定で、userが消えた時、関連するArticlesを消すようにしています。
userの方では、生成時にarticlesに複数の初期データを同時に作成するようにしています。
after_createのところ見てもらえば分かる通り、1〜5の文字を適当に突っ込んで5レコード作っています。
ここまで前準備。
本題。。。。
さて、ここでarticlesを消す前に、他のテーブルに移すとか、ログにはき出すなりをやりたいとします。
当然dependent指定されているため、after_destroyではarticles拾えなくなるため、before_destroyで拾うとします。
user.rbみてもらうばわかる通り、before_destroyで標準出力に出してあげるようにしています。
User.create(id:13, name:1) => #<User id: 13, name: "test"> User.find(13).articles => [#<Article id: 26, user_id: 13, text: "1">, #<Article id: 27, user_id: 13, text: "2">, #<Article id: 28, user_id: 13, text: "3">, #<Article id: 29, user_id: 13, text: "4">, #<Article id: 30, user_id: 13, text: "5">] User.find(13).destroy >> []
ファッ!?
なぜか空配列が帰ってきてしまいます。 pry眺めてると先にarticlesのDeleteが走っていることがわかります。
ちなみに、こう書けば空配列にはなりません。
class User < ActiveRecord::Base before_destroy do # こちらの定義を先にした p articles.map(&:text) end has_many :articles, dependent: :destroy after_create do articles.import(%w"1 2 3 4 5".map{|text| Article.new(text:text)}) end end >> ["1", "2", "3", "4", "5"]
なぜこのような挙動をするのか。
深くまで潜っていけば、has_manyにdependent: :destroyが指定されている場合、
lib/active_record/associations/builder/association.rbで以下のように、before_destroy時に関連するモデルを削除する処理が埋め込まれます。
def self.define_callbacks(model, reflection) add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] Association.extensions.each do |extension| extension.build model, reflection end end
さて、ここで思い出してもらいたい。
modelに対するcallbackは複数回指定することができます。
さっきのuser.rbにこれ追加します。
before_destroy do p 1 end before_destroy do p 2 end >> [] 1 2
追加したbefore_destroyを逆にして再度実行すると…
[] 2 1
そうです、大体見えてみた。
先にbefore_destroyを定義した順番に実行されていくのです。
最初に記載していたコードを見てください。
has_manyの定義が先に書かれています。そのため、has_many内で追加されていたdependent: :destroyのbefore_destroyが先に実行されていたために、空配列になっていたのです(`・ω・´)
これで地味にハマりました。
でもrubyは上から順番に評価されていくので、原因が分かってさえすれば理屈はだいたいわかりますねー。
おわり。