`touch_attachment_records = false` 時にBlobアタッチがクラッシュする問題を修正

rails/rails

ActiveStorage.touch_attachment_recordsfalse に設定している環境で、既存のBlobをRecordにアタッチしようとすると ActiveRecord::ActiveRecordError が発生する問題が修正されました。修正は1行の追加ガードのみとシンプルですが、その背景にはActive Storageの自動保存(autosave)アーキテクチャの変遷があります。

背景

本不具合は #53623 による「BlobがAttachmentをautosaveしないようにする」リファクタリングの副作用として発生しました。#55144 で報告されたこの問題は、touch_attachment_recordsfalse に設定したアプリケーションのみに影響し、デフォルト値の true では再現しないという条件付きのバグでした。

#53623 以前の処理フローでは、BlobがAttachmentをautosaveする仕組みになっていたため、Attachmentはアタッチ処理の中で自動的に永続化されていました。具体的には次の順序で処理が行われていました:

  1. Blobを作成する
  2. BlobがAttachmentをautosaveする(Attachmentが永続化される)
  3. 永続化済みの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.rbtouch_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を活用している環境では影響のある修正であり、アップグレード時に確認すべき変更といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3d270eb3

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:filepath)とGitHubのPR/Issue番号リンク([#123](URL))が、ガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「autosave」「コールバック」「リグレッションテスト」などの専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合した内容になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各パラグラフがトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容(`blob.rb`の変更、`attachment_test.rb`の追加)を正確に反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Active StorageやActive Recordに関連する技術用語(`touch`, `new_record?`, `autosave`など)が、PRの文脈と一般の用法に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「autosave廃止により未永続化レコードへのtouchが発生し例外が起きる」という問題の根本原因から、「`new_record?`でガードする」という解決策まで、技術的な説明は論理的で正確です。

事実の突合 ⚠ WARNING

PR情報による主張の裏付け(ハルシネーション検出)

PRのDescriptionには「an exception」としか書かれていないのに対し、記事では「`ActiveRecord::ActiveRecordError: Cannot touch on a new or destroyed record object`」という具体的な例外クラス名を記載しています。これは技術的に正しい補足ですが、厳密にはPR情報外からの追加情報です。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#55156)や関連Issue番号(#55144, #53623)が正確に記載・リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRのタイトルに問題の発生条件(`touch_attachment_records = false`)を加えており、PRの内容をより正確かつ具体的に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンのサポート状況やリリース予定など、PR情報に基づかない外部知識の追加は見られませんでした。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「#53623 以前」「#53623 で...廃止された」など、PRの文脈に沿った正確な時間表現が使われています。