`becomes` でSTI変換後も `marked_for_destruction` が引き継がれるように修正
ActiveRecord::Persistence#becomes を使ったSTI(Single Table Inheritance)モデルの型変換において、marked_for_destruction フラグが失われるバグが修正されました。これにより、一括操作でのネストされたフォームを含む複雑なユースケースで、意図した破棄処理が確実に引き継がれます。
背景
becomes メソッドは、STIモデルを別のサブクラスへ型変換する際に使われ、new_record?・previously_new_record?・destroyed? といったレコードの状態を引き継ぐ設計になっていました。しかし、accepts_nested_attributes_for と組み合わせたマスアサインメントの文脈では mark_for_destruction が呼ばれることがあり、becomes 後にそのフラグが消えてしまうという問題がありました。
結果として、becomes で型変換したオブジェクトをそのまま save しても、本来削除されるべきレコードが削除されないという不整合が生じていました。
技術的な変更
activerecord/lib/active_record/persistence.rb の becomes メソッドに1行が追加され、@marked_for_destruction インスタンス変数が変換先のオブジェクトへコピーされるようになりました。
変更前:
becoming.instance_variable_set(:@new_record, new_record?)
becoming.instance_variable_set(:@previously_new_record, previously_new_record?)
becoming.instance_variable_set(:@destroyed, destroyed?)
becoming.errors.copy!(errors)
変更後:
becoming.instance_variable_set(:@new_record, new_record?)
becoming.instance_variable_set(:@previously_new_record, previously_new_record?)
becoming.instance_variable_set(:@destroyed, destroyed?)
becoming.instance_variable_set(:@marked_for_destruction, marked_for_destruction?)
becoming.errors.copy!(errors)
追加されたテストでは、topics(:first) に対して mark_for_destruction を呼んだ後、becomes(Reply) で変換したオブジェクトが marked_for_destruction? を保持していることを確認しています。これにより、リグレッションを防ぐテストカバレッジが確保されています。
設計判断
既存の状態コピーパターンと完全に一致する形で修正が加えられました。becomes では以前から @new_record・@previously_new_record・@destroyed の各インスタンス変数を instance_variable_set でコピーしており、今回の @marked_for_destruction はそれらと同列のレコード状態として扱われています。実装の一貫性を保ちつつ、最小限の変更でバグを修正しているといえます。
また、PR説明にはアソシエーションの変更を保持することへの言及もありますが、今回のDiffに含まれているのは marked_for_destruction の修正のみです。変更は既存のパターンに沿った1行追加に留まり、副作用のリスクが最小化されています。
まとめ
becomes が引き継ぐレコード状態のリストに marked_for_destruction が加わったことで、STIモデルをまたいだ破棄処理の整合性が確保されました。既存の実装パターンへの自然な追従であり、ネストされたフォームや一括操作でのSTI利用における信頼性が高まります。