Polymorphic associations now honor custom primary keys via :inverse_of
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 でも同一メソッドを利用することで、コードの重複を排除しつつ既存動作を保護します。結果として、モデルごとに異なる識別子を持つアプリケーションでも、ポリモーフィック関連付けがシームレスに機能するようになりました。