primary_key の内部エイリアス @primary_key を除去し、定義との同期を簡素化
この変更により、primary_key と primary_key_definition が常に同期した状態で提供され、冗長な条件分岐が削除されます。結果として、Composite Primary Key 実装の保守性が向上します。
背景
従来、ActiveRecord は @primary_key というインスタンス変数と primary_key_definition オブジェクトを別々に保持していました。@primary_key は PRIMARY_KEY_NOT_SET という sentinel オブジェクトで初期化され、同期が取れないケースが散在していました。
primary_key メソッドは @primary_key が sentinel と等価かどうかを判定し、必要に応じて reset_primary_key を呼び出していました。primary_key_definition でも同様の判定が行われ、コードベース全体に似たような条件が散らばっていました。この構造は Composite Primary Key のリファクタリング時に特に脆弱さを露呈しました。
そのため、@primary_key と primary_key_definition を一本化し、条件ロジックを削減する必要がありました。PR #57638 で導入されたポリモーフィック ActiveRecord::Key を前提に、内部エイリアスの除去が検討されました。
技術的な変更
本 PR は activerecord/lib/active_record/attribute_methods/primary_key.rb に対し、@primary_key 関連のコードと sentinel PRIMARY_KEY_NOT_SET を削除しました。primary_key と primary_key_definition はそれぞれ単一の真理値ソースに統合されています。
変更前:
module ClassMethods
PRIMARY_KEY_NOT_SET = Object.new.freeze
def primary_key
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
@primary_key
end
def primary_key_definition
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
@primary_key_definition
end
def primary_key=(value)
@primary_key_definition = ActiveRecord::Key.for(value)
@primary_key = @primary_key_definition.name
# …
end
def inherited(base)
super
base.class_eval do
@primary_key = PRIMARY_KEY_NOT_SET
@primary_key_definition = nil
@attributes_builder = nil
end
end
end
変更後:
module ClassMethods
def primary_key
reset_primary_key unless @primary_key_definition
@primary_key_definition.name
end
def primary_key_definition
reset_primary_key unless @primary_key_definition
@primary_key_definition
end
def primary_key=(value)
@primary_key_definition = ActiveRecord::Key.for(value)
# @primary_key 削除に伴い設定不要
include CompositePrimaryKey if @primary_key_definition.composite?
end
def inherited(base)
super
base.class_eval do
@primary_key_definition = nil
@attributes_builder = nil
end
end
end
reset_primary_key は @primary_key_definition が未設定の場合にのみ呼び出され、primary_key は定義オブジェクトの name を直接返します。primary_key= では @primary_key への代入が廃止され、代わりに ActiveRecord::Key が生成されて保持されます。inherited でも sentinel の設定が削除され、継承時の状態がクリーンに初期化されます。
これにより、primary_key と primary_key_definition が常に同一オブジェクトを参照し、外部から取得した値に不整合が生じません。既存の API(model.primary_key や model.id 系)は振る舞いを変えずに利用できます。内部実装がシンプルになることで、将来の Composite Primary Key の拡張が容易になります。
内部変数の削減と条件分岐の統一により、コードベースの可読性と保守性が向上しました。
設計判断
設計上、primary_key の唯一の情報源として primary_key_definition を選択したことが本変更の核となります。
ActiveRecord::Key がポリモーフィックに振る舞うことで、単一キーだけでなく Composite Primary Key も同一インターフェイスで扱えるようになり、別個の ivar を持つ必要がなくなります。PRIMARY_KEY_NOT_SET のような sentinel を排除したことで、初期化ロジックが単純化され、未知の状態を判定する余計なコードが削除されました。
結果として、既存の公開インターフェイスは保持しつつ内部実装が一元化され、将来的なキー関連機能の拡張やバグ修正が局所化しやすくなります。
まとめ
@primary_key エイリアスと sentinel の除去により、primary_key と primary_key_definition が常に同期した状態で提供され、コードの複雑性が大幅に低減しました。このシンプル化は Composite Primary Key の保守・拡張を容易にし、Rails 全体の設計一貫性にも寄与します。