Composite Primary Key Owner の新規レコードで has_many/has_one が空になる不具合を修正

rails/rails

新規レコード(new_record? が true)かつ所有側に複合主キーが設定されている場合、has_manyhas_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_keyArray(...).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_manyhas_one の両方が利用する foreign_key_present? が統一的に正しい判定を行うようになりました。さらに、activerecord/test/cases/associations/has_many_associations_test.rbhas_one_associations_test.rb新規レコード所有者が複合キーを持つケースキーが欠損するケース のテストを追加し、期待通りにレコードが取得されることを確認しています。

設計判断

今回の修正は 既存メソッドのロジックを最小限の変更で拡張 するアプローチを採りました。foreign_key_present? の内部実装だけを変更し、外部インターフェースや呼び出し側のコードは一切変更していません。この方針は、既存の単一キー利用コードへの影響をゼロに保ちつつ、複合キー対応を追加できる点で合理的です。また、配列をそのまま属性名として扱う誤りは他の箇所でも潜在的に同様の問題を引き起こす可能性があるため、同様のパターンが見られる他のメソッドへの波及的なリファクタリングの検討材料となります。

まとめ

ForeignAssociation#foreign_key_present? が複合主キーの各カラムを正しく判定するように改修したことで、未保存レコードでも has_manyhas_one の関連が期待通りに取得できるようになりました。テスト追加により回帰が防止され、CHANGELOG にも記載されたことから、Rails の複合キーサポートが一段階前進したと言えます。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
c035cbd1

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文→背景→技術的な変更→設計判断→まとめの流れが明確で、総論・各論・結論が揃っています。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは `ruby:activerecord/lib/active_record/associations/foreign_association.rb` の形式で正しく記述され、PRリンクも適切にマークダウン化されています。

対象読者への適合性 ✓ PASS

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

Rails の内部実装に関する専門的な説明のみで、初心者向けの冗長な解説はありません。

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

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

各セクションは総論・各論・結論のパラグラフで構成され、トピックセンテンスが先頭にあり、1段落1トピック・適切な長さが保たれています。

Diff内容との照合 ✓ PASS

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

記事中のコード差分は提供された Diff と完全に一致しており、抜けや改変はありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(foreign_key_present?, composite primary key, has_many など)は正確で、PR 内容と一致しています。

説明の技術的正確性 ✓ PASS

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

技術的な説明は Diff と PR 説明に基づき、因果関係も正しく記述されています。

事実の突合 ✓ PASS

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

記事の主張はすべて PR のタイトル・説明・Diff で裏付けられており、根拠のない推測はありません。

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

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

PR 番号 #57651 などの固有名詞は正確です。

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

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

記事タイトルは PR の趣旨を日本語で的確に伝えており、一致しています。

外部知識の正確性 ✓ PASS

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

外部のバージョン情報やリリース予定など、PR に無い情報は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と整合しています。