STI変換時にActiveStorageの添付ファイル変更を保持する
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をオーバーライドして変換後のインスタンスに変更を引き継ぐようにしています。
CreateOne・CreateMany・DeleteOne・DeleteManyのattr_readerをattr_accessorに変更:
# 変更前
attr_reader :name, :record, :attachable
# 変更後
attr_reader :name, :attachable
attr_accessor :record
4つすべてのChangeクラスで同じパターンが適用されています。nameと各クラス固有の属性(attachable、attachablesなど)は読み取り専用のまま維持し、recordのみを書き換え可能にしています。
model.rbにbecomesのオーバーライドを追加:
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オブジェクトのrecordをbecameに更新し、そのHashをbecame.attachment_changes=で代入しています。@attachment_changes&.each_valueの&.により、添付ファイル変更が存在しない場合はnilが代入されますが、これはattachment_changesのデフォルトが@attachment_changes ||= {}のため影響ありません。
テストも追加されています。one_test.rbではhas_one_attached、many_test.rbではhas_many_attachedそれぞれに対して、STI変換後にattached?がtrueになることを検証しています。
設計判断
この変更は、Changeオブジェクトを再生成するのではなく、既存オブジェクトのrecord参照のみを更新する、シンプルで影響範囲の小さいアプローチです。
各Changeクラス(CreateOne・CreateMany・DeleteOne・DeleteMany)に対してattr_readerからattr_accessorへの変更を統一的に適用することで、recordの差し替えを可能にしています。変更の中心となるbecomesオーバーライドはtapを使ってActiveRecordのsuper呼び出しに処理を連鎖させており、既存のbecomesの動作を損なうことなく添付ファイルの引き継ぎを追加しています。
attr_writer :attachment_changesの追加も、このアプローチを成立させるために必要です。これまで@attachment_changesはattachment_changesのlazy initializerを通じてのみアクセスされていましたが、外部から代入できるようにすることでbecomesでの引き継ぎが可能になっています。
まとめ
本PRは、ActiveStorageのChangeオブジェクトが持つrecord参照の不変性を解除し、becomesのオーバーライドで引き継ぎロジックを追加することで、STI変換時の添付ファイル消失バグを修正しました。変更は既存のChangeクラスのインターフェースに最小限の修正を加えるにとどまっており、becomesを使わない通常のフローには一切影響を与えません。