belongs_to アソシエーションの複合主キーでのグループ計算を修正

rails/rails

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.rbexecute_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 の両方で正しく機能することが確認されています。この修正により、複合主キーを利用するアプリケーションでもグルーピング計算が一貫して利用できるようになりました。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断、まとめの5セクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックのファイル名付きハイライトは正しい形式ですが、PRリンクが `[PR #57532](URL)` となっており、要件の `[#57532](URL)` 形式と異なります。

対象読者への適合性 ✓ PASS

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

Rails 開発者向けの専門的な記述で、初心者向けの余計な説明はありません。

パラグラフ・ライティング ⚠ WARNING

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

各セクションに総論・各論はありますが、背景や設計判断などで明確な結論パラグラフが欠如している箇所があります。また、いくつかの段落が6文以上で長くなりがちです。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff の変更内容と完全に一致しており、ファイルパスも正しいです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

原因、修正内容、テスト追加の説明が PR の記述と合致しています。

事実の突合 ✓ PASS

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

記事内のすべての主張は PR のタイトル、説明、Diff で裏付けられています。外部知識の付加はありません。

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

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

PR 番号 #57532 だけが記載されており正確です。その他数値的記述はありません。

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

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

日本語タイトルは PR の内容を適切に要約しています。

外部知識の正確性 ✓ PASS

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

記事は PR に記載された情報以外の外部知識を含んでいません。

時間表現の正確性 ✓ PASS

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

時間表現は使用しておらず、PR の記述と矛盾はありません。