アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

before_destory の prepend 指定について

ユーザーがブログを書けるアプリケーションとかで、ユーザーが退会したら、関連するブログを削除する指定をすることは良くあると思います。

以下のような設定です。

class User < ActiveRecord::Base
  has_many :blogs, dependent: :destroy
end
class Blog < ActiveRecord::Base
  belongs_to :user
end

退会しても、ブログ情報はアーカイブして持っていたいと思った時とかは before_destroy を使って、削除前にブログのアーカイブ処理をやろうとか思っている時には、ちょっと注意が必要です。何かと言うと、before_destroy が実行される時には、すでに Blog 情報は削除されているといった問題です。

class User < ActiveRecord::Base
  has_many :blogs, dependent: :destroy

  before_destroy :ekusdesu_taosenai

  private
  def ekusdesu_taosenai
    puts Blog.first.title
  end
end
class Blog < ActiveRecord::Base
  belongs_to :user
end

こんなコードを書いたとして、ユーザーを削除すると、エラーになります。

[1] pry(main)> u = User.new(name: "popo")
=> #<User:0x007fcd95dacf18 id: nil, name: "popo", created_at: nil, updated_at: nil>
[2] pry(main)> u.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "popo"], ["created_at", "2016-01-06 09:31:19.952772"], ["updated_at", "2016-01-06 09:31:19.952772"]]
   (2.0ms)  commit transaction
=> true
[3] pry(main)> b = Blog.new(title: "エクスデス強すぎ", user_id: u.id)
=> #<Blog:0x007fcd95c8d948 id: nil, title: "エクスデス強すぎ", created_at: nil, updated_at: nil, user_id: 18>
[4] pry(main)> b.save
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "blogs" ("title", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "エクスデス強すぎ"], ["user_id", 18], ["created_at", "2016-01-06 09:32:04.797796"], ["updated_at", "2016-01-06 09:32:04.797796"]]
   (2.1ms)  commit transaction
=> true
[5] pry(main)> u.destroy
   (0.1ms)  begin transaction
  Blog Load (0.2ms)  SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = ?  [["user_id", 18]]
  SQL (1.1ms)  DELETE FROM "blogs" WHERE "blogs"."id" = ?  [["id", 12]]
  Blog Load (0.1ms)  SELECT  "blogs".* FROM "blogs"  ORDER BY "blogs"."id" ASC LIMIT 1
   (1.9ms)  rollback transaction
NoMethodError: undefined method `title' for nil:NilClass
from /Users/namikata/work/before_destory/before_destory/app/models/user.rb:9:in `ekusdesu_taosenai'

この問題を回避するには before_destroy に対して prepend: true を指定します。prepend は英単語としては存在してなくて pre + append を組み合わせた造語らしいです。指定すると Blog を削除する前に、処理を行ってくれるようになります。

class User < ActiveRecord::Base
  has_many :blogs, dependent: :destroy

  before_destroy :ekusdesu_taosenai, prepend: true

  private
  def ekusdesu_taosenai
    puts Blog.first.title
  end
end
[1] pry(main)> u = User.new(name: "popo")
=> #<User:0x007fcd93432f48 id: nil, name: "popo", created_at: nil, updated_at: nil>
[2] pry(main)> u.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "popo"], ["created_at", "2016-01-06 09:29:40.860009"], ["updated_at", "2016-01-06 09:29:40.860009"]]
   (2.0ms)  commit transaction
=> true
[3] pry(main)> b = Blog.new(title: "エクスデス強すぎ", user_id: u.id)
=> #<Blog:0x007fcd95e97018 id: nil, title: "エクスデス強すぎ", created_at: nil, updated_at: nil, user_id: 17>
[4] pry(main)> b.save
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "blogs" ("title", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "エクスデス強すぎ"], ["user_id", 17], ["created_at", "2016-01-06 09:30:03.992078"], ["updated_at", "2016-01-06 09:30:03.992078"]]
   (2.0ms)  commit transaction
=> true
[5] pry(main)> u.destroy
   (0.1ms)  begin transaction
  Blog Load (0.1ms)  SELECT  "blogs".* FROM "blogs"  ORDER BY "blogs"."id" ASC LIMIT 1
エクスデス強すぎ
  Blog Load (0.1ms)  SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = ?  [["user_id", 17]]
  SQL (0.4ms)  DELETE FROM "blogs" WHERE "blogs"."id" = ?  [["id", 11]]
  SQL (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 17]]
   (2.7ms)  commit transaction
=> #<User:0x007fcd93432f48 id: 17, name: "popo", created_at: Wed, 06 Jan 2016 09:29:40 UTC +00:00, updated_at: Wed, 06 Jan 2016 09:29:40 UTC +00:00>

参考にしたサイト

Active Record Callbacks