STI変換時にActiveStorageの添付ファイル変更を保持する

rails/rails

becomesメソッドでレコードをSTIサブクラスに変換した際、未保存のActiveStorage添付ファイル変更が失われるバグが修正されました。attachment_changesの引き継ぎと、各Changeクラスのrecord参照の更新をbecomesのオーバーライドで実現しています。

背景

Single Table Inheritance(STI) を使用するアプリケーションでは、becomesメソッドを使って同一レコードを別のサブクラスのインスタンスに変換するパターンが一般的です。しかし #45778 で報告されたように、ActiveStorageの添付ファイルを設定した後にbecomesで別クラスに変換すると、その添付ファイル変更が消えてしまうという問題がありました。

問題の再現手順は単純です。Commentモデルにhas_one_attached :imageを持つ構成で、Comment.newにファイルを添付した後、comment.becomes(ModeratedComment)で変換すると、moderated_comment.image.attached?falseを返します。これはUser/SpecialUserのようなどのSTI構成でも同様に発生していました。

未保存の添付ファイル変更は@attachment_changesというインスタンス変数のHashで管理されていますが、becomesが新しいインスタンスを生成する際にこのHashが引き継がれないことが根本原因でした。

技術的な変更

変更は2つの層で行われています。第一に各Changeクラスのrecord属性を書き換え可能にし、第二にbecomesをオーバーライドして変換後のインスタンスに変更を引き継ぐようにしています。

CreateOneCreateManyDeleteOneDeleteManyattr_readerattr_accessorに変更:

# 変更前
attr_reader :name, :record, :attachable

# 変更後
attr_reader :name, :attachable
attr_accessor :record

4つすべてのChangeクラスで同じパターンが適用されています。nameと各クラス固有の属性(attachableattachablesなど)は読み取り専用のまま維持し、recordのみを書き換え可能にしています。

model.rbbecomesのオーバーライドを追加:

attr_writer :attachment_changes # :nodoc:

def becomes(klass) # :nodoc:
  super.tap do |became|
    attachment_changes = @attachment_changes&.each_value do |change|
      change.record = became
    end
    became.attachment_changes = attachment_changes
  end
end

処理の流れは以下のとおりです。superでActiveRecordのbecomesを呼び出して新しいインスタンス(became)を生成した後、元インスタンスの@attachment_changesに含まれる各Changeオブジェクトのrecordbecameに更新し、そのHashをbecame.attachment_changes=で代入しています。@attachment_changes&.each_value&.により、添付ファイル変更が存在しない場合はnilが代入されますが、これはattachment_changesのデフォルトが@attachment_changes ||= {}のため影響ありません。

テストも追加されています。one_test.rbではhas_one_attachedmany_test.rbではhas_many_attachedそれぞれに対して、STI変換後にattached?がtrueになることを検証しています。

設計判断

この変更は、Changeオブジェクトを再生成するのではなく、既存オブジェクトのrecord参照のみを更新する、シンプルで影響範囲の小さいアプローチです。

各Changeクラス(CreateOneCreateManyDeleteOneDeleteMany)に対してattr_readerからattr_accessorへの変更を統一的に適用することで、recordの差し替えを可能にしています。変更の中心となるbecomesオーバーライドはtapを使ってActiveRecordのsuper呼び出しに処理を連鎖させており、既存のbecomesの動作を損なうことなく添付ファイルの引き継ぎを追加しています。

attr_writer :attachment_changesの追加も、このアプローチを成立させるために必要です。これまで@attachment_changesattachment_changesのlazy initializerを通じてのみアクセスされていましたが、外部から代入できるようにすることでbecomesでの引き継ぎが可能になっています。

まとめ

本PRは、ActiveStorageのChangeオブジェクトが持つrecord参照の不変性を解除し、becomesのオーバーライドで引き継ぎロジックを追加することで、STI変換時の添付ファイル消失バグを修正しました。変更は既存のChangeクラスのインターフェースに最小限の修正を加えるにとどまっており、becomesを使わない通常のフローには一切影響を与えません。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
5e81068f

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の3部構成が明確です。リード文で要旨を述べ、背景、技術詳細、設計判断、まとめが適切に配置されており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのIssueリンク記法が正しく使用されており、コードの出典と関連情報が明確です。

対象読者への適合性 ✓ PASS

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

STIやActiveStorageの内部実装に関するトピックを、専門知識を持つエンジニア向けに過不足なく解説しており、対象読者に完全に適合しています。

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

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

各セクション、各パラグラフがトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、提供されたDiff情報と完全に一致しています。ファイル名も正確で、変更内容を忠実に反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「STI」「becomes」「@attachment_changes」などの技術用語が文脈に応じて正確に使用されており、技術的な信頼性が高い記事です。

説明の技術的正確性 ✓ PASS

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

コード変更の意図や動作フローに関する説明は、Diff情報と照らし合わせて技術的に正確かつ論理的です。特に`becomes`のオーバーライドに関する解説は秀逸です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのタイトル、Description、Diff内のコード、テストコードによって裏付けられています。ハルシネーション(捏造)は一切見られません。

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

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

PR番号(#46486)や関連Issue番号(#45778)などの固有名詞はすべて正確に記載されており、参照情報として信頼できます。

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

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

記事のタイトル「STI変換時にActiveStorageの添付ファイル変更を保持する」は、PRの主題を的確に要約しており、内容との整合性が取れています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれないバージョンサポート状況やリリース日程などの外部知識は記載されておらず、事実に基づいた信頼性の高い記事となっています。

時間表現の正確性 ✓ PASS

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

「修正されました」といった過去形の表現が適切に使われており、PRがマージされた事実を正確に伝えています。時間表現の歪曲はありません。