Composite Primary Key Owner の新規レコードで has_many/has_one が空になる不具合を修正
新規レコード(new_record? が true)かつ所有側に複合主キーが設定されている場合、has_many と has_one の関連がクエリされず空結果になるバグを解消しました。これにより、複合キーを利用したモデルでも期待通りに関連データを取得できるようになります。
背景
has_many / has_one が新規レコードで空になる 問題は、所有モデルが複合主キー(例: [:author_id, :id])を持つときにのみ発生していました。単一主キーの場合は attribute_present? が正しく動作し、関連はデータベースに問い合わせられますが、複合キーでは配列全体が属性名として扱われ、常に false が返っていました。この挙動は ActiveRecord::Associations::ForeignAssociation#foreign_key_present? が owner.attribute_present?(reflection.active_record_primary_key) と記述していたことが根本原因です。
結果として、new_record? && !foreign_key_present? が常に真となり、null_scope? が発火してクエリがスキップされました。そのため、Cpk::Book.new(id: [1,10]).chapters などはデータベースに問い合わせず [] を返していました。PR の目的は、このロジックを複合キーでも正しく判定できるように修正し、テストで挙動を検証することです。
技術的な変更
ForeignAssociation#foreign_key_present? の実装を変更し、複合キーの各カラムを個別にチェックするようにしました。具体的には配列化した reflection.active_record_primary_key を Array(...).all? で走査し、owner.attribute_present?(key) を各キーに対して呼び出すようにしています。単一キーの場合は配列に変換した結果が 1 要素になるため、既存の振る舞いはそのまま保持されます。
@@
- owner.attribute_present?(reflection.active_record_primary_key)
+ Array(reflection.active_record_primary_key).all? do |key|
+ owner.attribute_present?(key)
+ end
この変更に伴い、has_many と has_one の両方が利用する foreign_key_present? が統一的に正しい判定を行うようになりました。さらに、activerecord/test/cases/associations/has_many_associations_test.rb と has_one_associations_test.rb に 新規レコード所有者が複合キーを持つケース と キーが欠損するケース のテストを追加し、期待通りにレコードが取得されることを確認しています。
設計判断
今回の修正は 既存メソッドのロジックを最小限の変更で拡張 するアプローチを採りました。foreign_key_present? の内部実装だけを変更し、外部インターフェースや呼び出し側のコードは一切変更していません。この方針は、既存の単一キー利用コードへの影響をゼロに保ちつつ、複合キー対応を追加できる点で合理的です。また、配列をそのまま属性名として扱う誤りは他の箇所でも潜在的に同様の問題を引き起こす可能性があるため、同様のパターンが見られる他のメソッドへの波及的なリファクタリングの検討材料となります。
まとめ
ForeignAssociation#foreign_key_present? が複合主キーの各カラムを正しく判定するように改修したことで、未保存レコードでも has_many と has_one の関連が期待通りに取得できるようになりました。テスト追加により回帰が防止され、CHANGELOG にも記載されたことから、Rails の複合キーサポートが一段階前進したと言えます。