無気力生活 (ノ ´ω`)ノ ~゜

脱力系エンジニア。てきとーに生きてます。

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は上から順番に評価されていくので、原因が分かってさえすれば理屈はだいたいわかりますねー。

おわり。