何が起きるか
親レコードを消すと、子レコードも一緒に消えて便利な dependent: :destroy(いわゆる cascade 削除)。
でも子がコールバックを持っていると、削除のたびにそれが発火します。
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author, touch: true # 消すと親の更新日時を更新(いわゆる touch)
after_destroy :reindex_search # 1件消すたびに Elasticsearch を再インデックス(同期・重い)
end
author.destroy すると…
Author を1件destroy
└─ books が1000件あれば 1件ずつ destroy(cascade)
└─ そのたびに親を touch し、after_destroy で
Elasticsearch 更新が「直列」で1000回走る 😱
件数が少ないうちは気づかず、本番のデータ量で突然タイムアウトします。
対策:重い子だけ「コールバック無し」で先に消す
delete_all はコールバック(cascade も touch も)を発火させず、SQL 1発で消します。
ActiveRecord::Base.transaction do
Book.where(author_id: author.id).delete_all # ① 重い子を静かに一掃
author.destroy # ② もう子はいないので連鎖しない
end
# Elasticsearch の更新は捨てずに、あとで非同期・一括で1回だけ
ReindexJob.perform_later(author.id) # ③ トランザクションの外で
ポイント3つ
| やること | 理由 |
|---|---|
重い子を delete_all で先に消す |
コールバック(cascade / touch)を発火させない(destroy_all は発火する) |
| 本体はそのあと消す | 連鎖する子が残っていないので重い処理が走らない |
| 重い処理(ES再インデックス)は非同期ジョブで一括 | 「同期で1件ずつ」→「非同期でまとめて1回」に置き換え |
用語メモ
-
cascade(カスケード) … 親を消すと子も連鎖して消える挙動。
dependent: :destroyがこれ - touch … 中身は変えず「更新された印」として関連レコードの更新日時だけ更新すること
-
destroy= 1件ずつ読み込み、コールバックを呼ぶ(安全だが遅い) -
delete= SQLで直接、コールバックを呼ばない(速いが自己責任)
Top comments (0)