`touch_attachment_records = false` 時にBlobアタッチがクラッシュする問題を修正
ActiveStorage.touch_attachment_records を false に設定している環境で、既存のBlobをRecordにアタッチしようとすると ActiveRecord::ActiveRecordError が発生する問題が修正されました。修正は1行の追加ガードのみとシンプルですが、その背景にはActive Storageの自動保存(autosave)アーキテクチャの変遷があります。
背景
本不具合は #53623 による「BlobがAttachmentをautosaveしないようにする」リファクタリングの副作用として発生しました。#55144 で報告されたこの問題は、touch_attachment_records を false に設定したアプリケーションのみに影響し、デフォルト値の true では再現しないという条件付きのバグでした。
#53623 以前の処理フローでは、BlobがAttachmentをautosaveする仕組みになっていたため、Attachmentはアタッチ処理の中で自動的に永続化されていました。具体的には次の順序で処理が行われていました:
- Blobを作成する
- BlobがAttachmentをautosaveする(Attachmentが永続化される)
- 永続化済みのAttachmentに対して
touchを呼び出す
ステップ3はすでに永続化されたAttachmentへの touch であるため、技術的には正しく動作していました(ただしPR著者が指摘するように、この時点ではまだAttachmentにRecordが紐付いていないため空のRelationが返ることで実質的には無害でした)。
#53623 でautosaveが廃止されたことで、Blobのコールバック内でAttachmentが touch される時点では、Attachmentがまだ永続化されていない状態になりました。未永続化のレコードに対して touch を呼び出すと、Active Recordは ActiveRecord::ActiveRecordError: Cannot touch on a new or destroyed record object を送出します。
技術的な変更
activestorage/app/models/active_storage/blob.rb の touch_attachments プライベートメソッドに、Attachmentの永続化状態を確認するガード条件が1行追加されました。
変更前:
def touch_attachments
relation
end.each do |attachment|
attachment.touch
end
変更後:
def touch_attachments
relation
end.each do |attachment|
attachment.touch unless attachment.new_record?
end
attachment.new_record? を用いて未永続化のAttachmentをスキップするだけの変更です。これにより、direct uploadで作成されたBlobをRecordにアタッチする際、Attachmentが永続化されるより前にコールバックが発火しても例外が発生しなくなります。
テストは activestorage/test/models/attachment_test.rb に追加されました。create_blob_before_direct_upload を使ってdirect upload相当のBlobを事前作成し、ActiveStorage.with(touch_attachment_records: false) ブロック内でそのBlobをRecordに紐付けるフローが assert_nothing_raised で保護されています。これはバグの再現条件(direct upload + touch_attachment_records: false)を正確に反映したリグレッションテストです。
設計判断
既存のAttachment touch ロジックをスキップする方式 が採用されました。
Attachmentが未永続化の場合、touch を行う意味は本来ありません。Attachmentが永続化されるのはRecord保存時のautosaveフェーズ以降であり、その時点ではRecordもすでに存在しているため、touch_attachment_records = false の設定意図(不要なSQL発行の抑制)とも整合します。new_record? チェックを追加することで、コールバックの発火順序に依存せず安全に動作する実装になっています。
修正範囲を new_record? の1条件に絞った判断は、自動保存フローの変更(#53623)によって生じた「本来 touch すべきでないタイミングで呼び出される」という状況を最小の変更で正規化するものです。
まとめ
本修正は、#53623のアーキテクチャ変更によって生じた「Attachmentのライフサイクルとコールバックの発火タイミングのズレ」を new_record? の1行ガードで解消したものです。touch_attachment_records = false を使用しているアプリケーション、特にdirect uploadを活用している環境では影響のある修正であり、アップグレード時に確認すべき変更といえます。