Polymorphic associations now honor custom primary keys via :inverse_of

rails/rails

ActiveRecord now respects custom primary keys defined on the inverse side of a polymorphic association when :inverse_of is supplied. This enables each associated model to use its own identifier column (e.g., uuid or slug) without manual overrides.

背景

Historically, some models keep a unique identifier column separate from the default id, and the column names differ across models. The association layer reads attributes via _read_attribute for performance, which bypasses alias_attribute and therefore cannot resolve these custom identifiers. Consequently, applications had to monkey‑patch _read_attribute to make polymorphic belongs_to work with non‑standard primary keys.

Because polymorphic associations cannot infer an inverse_of automatically, the explicit :inverse_of option was previously ignored in many cases. This meant that even when developers specified the inverse, the custom primary key information was not propagated, leading to incorrect foreign‑key values (e.g., storing the default id instead of uuid). The new change remedies this limitation by making the association system honor the explicit inverse definition.

技術的な変更

The implementation now extends BelongsToReflection#association_primary_key to look up the inverse association when the reflection is polymorphic and an :inverse_of is present. If the inverse defines a :primary_key, that key is used; otherwise the original derivation path is followed. This logic centralizes primary‑key resolution and removes the special‑case handling that previously existed.

@@
-        if options[:primary_key]
-          @association_primary_key ||= ActiveRecord::Key.for(options[:primary_key]).name
+        if options[:primary_key]
+          @association_primary_key ||= ActiveRecord::Key.for(options[:primary_key]).name
+        elsif polymorphic? && options[:inverse_of] && klass
+          inverse = klass.reflect_on_association(options[:inverse_of])
+          if inverse && inverse.options[:primary_key]
+            ActiveRecord::Key.for(inverse.options[:primary_key]).name
+          else
+            derive_primary_key(klass) { |model| model.composite_query_constraints_list }
+          end
         else
           derive_primary_key(klass || self.klass) { |model| model.composite_query_constraints_list }
         end

AutosaveAssociation was simplified to reuse the new reflection method instead of duplicating primary‑key computation. The save_belongs_to_association path now calls reflection.association_primary_key(record.class) directly, reducing code churn and keeping the logic consistent.

@@
-            primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
+            primary_key = Array(reflection.association_primary_key(record.class)).map(&:to_s)
*** End of File ***

A suite of new tests validates the behavior: BelongsToPolymorphicInversePrimaryKeyTest creates Author and Person records with distinct identifier columns (author_code and external_id) and asserts that a PolymorphicComment stores the correct foreign‑key value for each type. A parallel AutosavePolymorphicInversePrimaryKeyTest confirms that autosave also respects the custom keys.

設計判断

The change prefers re‑using existing reflection utilities over embedding duplicate primary‑key logic in autosave. By extending association_primary_key with a polymorphic‑aware branch, the codebase gains a single source of truth for key resolution, enhancing maintainability. The design deliberately keeps backward compatibility: when no :inverse_of or custom primary key is present, the original path is unchanged, so existing applications experience no side effects.

Choosing to augment the reflection rather than introduce a new configuration key (:primary_key_for_inverse) respects the convention‑over‑configuration philosophy of Rails. It also limits the surface area of the public API, making the new capability discoverable through the already‑familiar :inverse_of option.

まとめ

この PR は、polymorphic association:inverse_of の組み合わせでカスタムプライマリーキーを正しく扱えるようにする重要な改善です。実装は BelongsToReflection#association_primary_key にロジックを集約し、AutosaveAssociation でも同一メソッドを利用することで、コードの重複を排除しつつ既存動作を保護します。結果として、モデルごとに異なる識別子を持つアプリケーションでも、ポリモーフィック関連付けがシームレスに機能するようになりました。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断(任意)、まとめが明確に区切られており、総論→各論→結論の流れが保持されている。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しいファイル名付きシンタックスハイライトを使用しているが、PRリンクが「[PR #57795]」という形式で、推奨される「[#57795]」の形式になっていない。

対象読者への適合性 ✓ PASS

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

専門的なRailsエンジニア向けの内容で、初心者向けの過度な説明はなく適切。

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

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

各セクションが総論・各論・結論の段落構成になり、トピックセンテンスで始まり、段落は6文未満で空行で区切られている。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと正確に一致しており、ファイル名・変更内容ともに合致している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語(polymorphic association、:inverse_of、primary_key など)は正しく使用されている。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの変更点と整合しており、根拠が示されている。

事実の突合 ✓ PASS

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

記述された事実はすべてPRタイトル、Description、Diffで裏付けられており、捏造や推測は見られない。

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

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

PR番号やファイル名などの固有名詞は正確。

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

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

記事タイトルはPRの趣旨を正確に反映している。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリース日程など、PRに無関係な外部知識は含まれていない。

時間表現の正確性 ✓ PASS

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

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