Polymorphic has_one の削除で残る type カラムを nullify する修正
Polymorphic has_one を代入や nil に設定した際、外部キーは NULL 化されるものの type カラム が旧値のまま残っていました。この不整合を解消し、dependent: :nullify と同様に両カラムを確実に NULL 化するよう変更が加えられました。
背景
これまで HasOneAssociation#nullify_owner_attributes は外部キーのみを nil に設定し、ポリモーフィックな has_one では型情報を保持したままにしていました。その結果、owner.thing = nil のように代入で削除した場合、<name>_id は NULL になる一方で <name>_type が古いクラス名を残し、データの一貫性が損なわれるケースがありました。
dependent: :nullify 経路では ForeignAssociation#nullified_owner_attributes が外部キーと併せて type カラムも nil にするため、2 つの「nullify」ルートで挙動が食い違っていました。この問題は、子側が逆方向の belongs_to を持たない 一方向的な polymorphic has_one で顕在化し、テストでも stale な type が観測されました。
技術的な変更
activerecord/lib/active_record/associations/has_one_association.rb の nullify_owner_attributes に、以下の一行が追加されました。
record[reflection.type] = nil if reflection.type.present?
この行は、reflection.type が存在する(すなわちポリモーフィックな関連)場合に型カラムを nil に設定します。非ポリモーフィックな has_one では reflection.type が nil になるため、処理は実質的に何もしません。したがって既存の API へは後方互換性を保ったまま動作します。
さらに activerecord/test/cases/associations/has_one_associations_test.rb にテスト test_replacing_a_polymorphic_has_one_nullifies_the_type_column が追加され、単方向ポリモーフィック has_one で owner.child = nil 後に両カラムが nil になることを検証しています。このテストは SQLite3、PostgreSQL、MySQL2 のすべてで成功し、修正の信頼性が確認されています。
設計判断
開発者は新たなフラグやメソッドを導入するよりも、既存の HasOneAssociation#nullify_owner_attributes に型クリア処理を直接組み込む方針を選択しました。これによりコードパスが統一され、dependent: :nullify と同一のロジックで属性をリセットできるようになり、メンテナンスコストが抑えられます。
修正は最小限の差分で済んでおり、非ポリモーフィックなケースへの副作用はありません。結果として、データベース上の整合性が向上し、既存アプリケーションの挙動に影響を与えることなくバグが解消されました。
まとめ
この PR は、Polymorphic has_one の代入や削除時に残っていた stale type カラムを HasOneAssociation#nullify_owner_attributes に型クリアロジックを追加することで、dependent: :nullify と同様の振る舞いを実現しました。テストの追加により多様な DB での動作が保証され、既存コードへの影響はなく安全にデータ整合性が保たれます。