何が起きるか 親レコードを消すと、子レコードも一緒に消えて便利な 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で直接、コールバックを呼ばない(速いが自己責任)