Composite Primary Key の inverse_of マッチング修正

rails/rails

この PR は、所有側が単一の id を持ち、関連側が複合主キーを宣言しているケースで inverse_of が正しく機能しない問題を修正します。 ActiveRecord::Associations::Association#matches_foreign_key? の比較ロジックを拡張し、テストとモデル定義も更新しています。

背景

inverse_of が機能しないと、belongs_to / has_many 間のオブジェクト同一性が保てず、期待したレコードが取得できません。 具体的には、所有クラスがスカラー id、関連クラスが複合主キー(例: [shop_id, order_id])を持つと、従来の比較は所有側のプライマリキー配列と外部キー配列を直接比較していたため不一致が生じていました。

この不一致は、PR #56664 で配列比較のロジックが修正されたものの、所有側がスカラー id のケースを網羅できていなかったことが根本原因です。 本 PR はその抜け漏れを埋め、inverse_of が正しく逆参照マッチングできるようにします。

技術的な変更

Association#matches_foreign_key? の内部比較対象を active_record_primary_key_valuesassociation_primary_key_values に置き換えました。 これにより、所有レコードのプライマリキーは reflection.active_record_primary_key から取得し、関連レコードは reflection.association_primary_key(record.class) から取得して個別に比較します。変更は以下の抜粋で確認できます(抜粋):

@@
-          foreign_key_values(record) == primary_key_values(owner)
+          foreign_key_values(record) == active_record_primary_key_values(owner)
@@
-          foreign_key_values(owner) == primary_key_values(record)
+          foreign_key_values(owner) == association_primary_key_values(record)
@@
-        def primary_key_values(record)
-          Array(reflection.active_record_primary_key).map { |key| record.read_attribute(key) }
+        def active_record_primary_key_values(record)
+          Array(reflection.active_record_primary_key).map { |key| record.read_attribute(key) }
         end
+
+        def association_primary_key_values(record)
+          Array(reflection.association_primary_key(record.class)).map { |key| record.read_attribute(key) }
+        end

テストも追加され、[shop_id, order_id] 形式の外部キーが所有側の [shop_id, id] と正しくマッチすることを検証しています。 代表的なテストコードは次の抜粋です(抜粋):

@@
   def test_belongs_to_a_model_with_composite_association_primary_key_sets_inverse_of
     cpk_order = cpk_orders(:cpk_groceries_order_1)
     store_id, order_id = cpk_order.id
     order = Cpk::NonCpkOrder.find(order_id)
     book = order.books_with_composite_primary_key.create!(id: [store_id, 4], title: "Book")
@@
     assert_same associated_order, associated_book.non_cpk_order
   end

モデル定義も更新され、BookWithNonCpkOrderNonCpkOrder に複合キーと inverse_of を明示的に設定しました。 変更点は以下の通りです(抜粋):

class BookWithNonCpkOrder < ActiveRecord::Base
  self.table_name = :cpk_books

  belongs_to :non_cpk_order, class_name: "Cpk::NonCpkOrder",
    foreign_key: [:shop_id, :order_id], primary_key: [:shop_id, :id], inverse_of: :books_with_composite_primary_key
end
class NonCpkOrder < Order
  self.primary_key = :id

  has_many :books_with_composite_primary_key, class_name: "Cpk::BookWithNonCpkOrder",
    foreign_key: [:shop_id, :order_id], primary_key: [:shop_id, :id], inverse_of: :non_cpk_order
end

設計判断

reflection が提供する active_record_primary_keyassociation_primary_key をそのまま利用する設計は、キー構成が将来的に拡張された場合でも同一ロジックで対応可能という拡張性をもたらします。 既存のメソッドシグネチャは変更せず、内部ロジックだけを差し替えることで後方互換性を確保しています。

配列を直接 read_attribute に渡す危険性を排除し、キーを個別に取得して比較することで、バグの根本原因である型不一致を防止しています。 この変更は inverse_of の正確な動作を保証しつつ、既存コードへの影響を最小限に抑えるというトレードオフを適切に取っています。

まとめ

所有側がスカラー id、関連側が複合主キーを持つケースでも inverse_of が正しく機能するようになり、アソシエーションの同一性が保証されます。 比較ロジックの細分化とテスト追加により、ActiveRecord のアソシエーション機構が複合キーに対しても一貫した振る舞いを提供できるようになりました。

記事メタデータ

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

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断(任意)及びまとめが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは ```ruby:ファイルパス の形式で正しく記述され、PRリンクは [#57784](URL) の形式で正しくリンク化されています。

対象読者への適合性 ✓ PASS

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

Rails の内部実装に詳しいエンジニア向けの内容で、初心者向けの過度な説明はありません。

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

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

各セクションは総論・各論・結論の段落構成になっており、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードスニペットは提供されたDiffと完全に一致し、追加・削除行も正しく示されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`inverse_of`、`primary_key`、`association_primary_key` などの用語は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な変更点とその影響は PR の説明と整合しており、誤った主張は見られません。

事実の突合 ✓ PASS

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

全ての事実は PR タイトル、説明、Diff に裏付けられており、外部知識や推測は含まれていません。

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

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

PR 番号 #57784 およびコード中のファイル名・行番号は正確です。

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

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

記事タイトルは PR の内容「inverse matching for association composite primary keys」を正しく日本語化して表現しています。

外部知識の正確性 ✓ PASS

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

記事内に PR 以外のバージョン情報やリリース予定等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記載と一致しています。