has_and_belongs_to_many の composite association_foreign_key が配列で返るように修正
has_and_belongs_to_many が複合外部キーを受け取る際、Reflection#association_foreign_key が文字列化された配列を返していた不整合を解消しました。これにより、foreign_key と同様に配列で取得でき、リフレクションの利用が安全になります。
背景
問題点:has_and_belongs_to_many は foreign_key: と association_foreign_key: に複合キー(配列)を指定できるものの、Reflection#foreign_key は配列で返すのに対し、Reflection#association_foreign_key は内部で #to_s を呼び出し、"[:blog_id, :tag_id]" という文字列を返していました。結果として、リフレクションを介したスキーマ探索やメタプログラミングが期待通りに動作せず、複合 HABTM の introspection が不可能でした。
影響範囲:アプリケーションコードが reflection.association_foreign_key を利用して外部キー情報を取得するケース(例:動的なクエリ生成、スキーマドキュメント生成)で、複合キーが正しく扱えませんでした。foreign_key との API 不整合が混乱の原因となっていました。
技術的な変更
Reflection の実装修正:activerecord/lib/active_record/reflection.rb の association_foreign_key メソッドを以下のように変更しました。
@@
- @association_foreign_key ||= -(options[:association_foreign_key]&.to_s || class_name.foreign_key)
+ @association_foreign_key ||= ActiveRecord::Key.for(options[:association_foreign_key] || class_name.foreign_key).name
- 旧実装は
options[:association_foreign_key]&.to_sで文字列化していました。 - 新実装は
ActiveRecord::Key.for(...).nameを使用し、複合キーでも配列の文字列要素(["blog_id", "tag_id"])を返すよう統一しました。
テストモデルの追加:activerecord/test/models/sharded/blog_post.rb に、複合外部キーを持つ HABTM 関係 tags_with_composite_fk を追加しました。
@@
- has_many :tags, through: :blog_post_tags
+ has_many :tags, through: :blog_post_tags
+
+ has_and_belongs_to_many :tags_with_composite_fk,
+ class_name: "Sharded::Tag",
+ join_table: "sharded_blog_posts_tags",
+ foreign_key: [:blog_id, :blog_post_id],
+ association_foreign_key: [:blog_id, :tag_id]
リフレクションテストの拡張:activerecord/test/cases/reflection_test.rb に、foreign_key と association_foreign_key がともに配列を返すことを確認するテスト test_habtm_composite_keys_are_returned_as_arrays を追加しました。
@@
- def test_has_many_foreign_key_derived_from_inverse_with_composite_foreign_key
+ def test_habtm_composite_keys_are_returned_as_arrays
+ reflection = Sharded::BlogPost.reflect_on_association(:tags_with_composite_fk)
+
+ assert_equal ["blog_id", "blog_post_id"], reflection.foreign_key
+ assert_equal ["blog_id", "tag_id"], reflection.association_foreign_key
+ end
これらの変更により、association_foreign_key がスカラーキーの場合は従来どおり文字列を、複合キーの場合は 配列 を返すよう統一されました。
設計判断
一貫性の重視:foreign_key で既に ActiveRecord::Key.for(...).name が使用されていることから、association_foreign_key でも同様のロジックを採用することで API の一貫性を確保しました。新たに専用ヘルパーを作るよりも、既存のキー抽象化クラスを再利用する方が保守コストが低く、将来的なキー拡張にも自然に対応できます。
後方互換性の維持:スカラーキーでは挙動が変わらないため、既存アプリケーションへの影響はありません。association_foreign_key が文字列化された配列を返すケースは非推奨であり、テストでの期待値が変更されただけです。したがって、マイナーバージョンでの安全な修正と位置付けられます。
最小限の差分:変更は1行のロジック差分とテスト追加のみで、パフォーマンスやメモリ使用量に実質的な影響はありません。リフレクションの結果を利用するコードは、期待通りの型(Array)を受け取れるようになるだけです。
まとめ
今回の PR は has_and_belongs_to_many の複合外部キーに対するリフレクション API の不整合を解消 し、foreign_key と association_foreign_key が同一形式(スカラーは文字列、複合は配列)で返るよう統一しました。実装は ActiveRecord::Key.for の再利用という最小限の変更で、既存コードへの後方互換性を保ちつつ、メタプログラミングやスキーマ分析の信頼性を向上させます。