belongs_to アソシエーションの複合主キーでのグループ計算を修正
Rails の belongs_to アソシエーションを通じて 複合主キー を持つモデルで group(:assoc).count を実行すると ArgumentError が発生し、結果が得られませんでした。この PR はその障害を取り除き、従来と同様にアソシエーションレコードで結果がキー付けされるようにします。
背景
Book.group(:order).count のように belongs_to 先が複合主キー (shop_id, id) を持つ Order モデルを対象とした場合、execute_grouped_calculation が取得する key_ids が単一カラムの配列だけになるため、where(["shop_id", "id"] => …) に渡すと配列の形が合わず ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array がスローされました。単一カラムキーのケース (Comment.group(:post)) は問題なく動作していたため、複合キーだけが抜け落ちていたことが根本原因です。PR の変更はこの不整合を解消し、複合キーでも正しく結果をグルーピングできるようにすることが目的です。
技術的な変更
activerecord/lib/active_record/relation/calculations.rb の execute_grouped_calculation 内で、key_ids の取得ロジックに条件分岐を追加しました。モデルが composite_primary_key? を持つ場合は、group_aliases 全体をマッピングしてタプル(配列)を収集し、そうでない場合は従来通り単一カラムを取得します。この分岐は既存コードに最小限の変更で済むよう設計されています。
変更前:
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
変更後:
key_ids = if association.klass.base_class.composite_primary_key?
calculated_data.collect { |row| group_aliases.map { |group_alias| row[group_alias] } }
else
calculated_data.collect { |row| row[group_aliases.first] }
end
この変更により、key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) が期待通りのタプル配列を受け取り、index_by(&:id) が正しいキーでレコードをマッピングできるようになります。単一キーの場合は挙動が変わらないため、既存のアプリケーションへの影響はありません。
テストも activerecord/test/cases/calculations_test.rb に 複合主キーを持つ belongs_to での group/count を検証するケースが追加され、SQLite と PostgreSQL の双方で緑化されています。
設計判断
今回の修正は モデルが複合主キーかどうかを判定してロジックを分岐 させるというシンプルなアプローチを取っています。新たにメソッドやクラスを導入せず、composite_primary_key? という既存ヘルパーを活用することで、コードベースへの侵入度を最低限に抑えました。また、単一キー向けの既存パスをそのまま残すことで 後方互換性 を保証しています。PR の議論でも、別途設定キーを増やす案が検討されたものの、既存インターフェースを拡張する方針が選択されました。この設計は、ActiveRecord の抽象化レイヤーにおける「キー取得はモデル側が把握すべき」という原則に沿ったものと言えます。
まとめ
belongs_to が指す複合主キーのモデルでも group(:assoc).count が正常に動作するよう、execute_grouped_calculation のキー取得ロジックが拡張されました。単一キーへの既存挙動は変わらず、テストによって SQLite と PostgreSQL の両方で正しく機能することが確認されています。この修正により、複合主キーを利用するアプリケーションでもグルーピング計算が一貫して利用できるようになりました。