`dependent: :purge` が期待通りにレコード削除時のファイルを完全削除するよう修正
Active Storage で dependent: :purge を指定しても添付ファイルが削除されずデタッチのみ行われていたバグを修正しました。これにより dependent: :purge / :purge_later / :detach の3つのオプションがすべて期待通りに動作するようになります。
背景
has_one_attached / has_many_attached の dependent オプションが一部のケースで期待通りに動作しないバグが #36423 で報告されていました。Active Storage では添付ファイルの削除方法として purge(同期削除)、purge_later(非同期削除)、detach(レコードのみ削除、ファイルは残す)の3種類が提供されています。
問題は dependent: :purge を指定した場合に発生していました。ユーザーレコードを削除すると、添付ファイルはストレージから削除されず「デタッチ」のみが行われていました。つまり ActiveStorage::Attachment レコードは消えてもファイル本体(ActiveStorage::Blob)はストレージに残留するという状態です。
Issue の指摘によると、問題の根本は lib/active_storage/attached/macros.rb のコールバック定義にあり、dependent: :purge_later の場合のみ after_destroy_commit で purge_later を呼び出し、それ以外はすべて before_destroy で detach を呼び出す実装になっていたため、:purge が :detach と同じ挙動になっていました。
技術的な変更
修正の核心は ActiveStorage::Attachment モデルの after_destroy_commit コールバックにあります。purge_dependent_blob_later というメソッド名と実装が :purge_later 専用になっていたものを、:purge もハンドリングできるよう拡張しました。
変更前:
after_destroy_commit :purge_dependent_blob_later
def purge_dependent_blob_later
blob&.purge_later if dependent == :purge_later
end
変更後:
after_destroy_commit :purge_dependent_blob
def purge_dependent_blob
if dependent == :purge_later
blob&.purge_later
elsif dependent == :purge
blob&.purge
end
end
コールバックメソッド名も purge_dependent_blob_later から purge_dependent_blob に改名され、役割がより正確に表現されています。dependent == :detach や dependent == false の場合は条件に合致しないためファイルは削除されず、デタッチのみが呼び出し元の処理に委ねられます。
テストフィクスチャには dependent: :purge を指定した新たなアタッチメント定義が追加されました。
has_one_attached :icon, dependent: :purge
has_many_attached :favorites, dependent: :purge
これらを用いたテストケースがそれぞれ追加されており、レコード削除後に ActiveStorage::Blob レコードとストレージ上のファイルの両方が存在しないことを検証しています。
設計判断
after_destroy_commit コールバックに処理を集約する方式が採用されました。修正前のコードは before_destroy + after_destroy_commit に処理が分散していましたが、このPRでは after_destroy_commit 側の purge_dependent_blob メソッドで全ての dependent パターンを一元的に判定するよう整理されています。
メソッドの改名(purge_dependent_blob_later → purge_dependent_blob)も重要な変更です。旧名称は「後で削除する」という意味を示唆していましたが、同期削除(:purge)も扱うようになったため、名称が実態とずれていました。新名称はモードに依存しない汎用的な表現になっています。
まとめ
本PRは1つのプライベートメソッドへの条件分岐追加という最小限の変更で、dependent: :purge / :purge_later / :detach の3オプションが設計通りに動作することを保証しました。メソッドの改名も合わせて行われ、コードの意図がより明確になっています。dependent: :purge を利用していたアプリケーションは、このバグ修正によりレコード削除時に添付ファイルが期待通りストレージから同期削除されるようになります。