disable_joins:true の through アソシエーションで複合キーが空結果になるバグを修正
has_many / has_one :through に disable_joins: true を指定した際、ソースモデルが複合主キーを持つと結果が空配列になる不具合を解消しました。これにより、disable_joins を利用したパフォーマンス最適化が安全に適用可能になります。
背景
disable_joins: true が付与されたアソシエーションは、結合クエリを分割して個別に取得し、最終的に in‑memory でのグルーピング にフォールバックします。ここで順序付け (order) が存在し、最終スコープに order が無い場合、ActiveRecord::DisableJoinsAssociationRelation#load が records.group_by を実行します。その際に使用されるキーは reflection.join_primary_key で、複合キーの場合は 配列(例: ["shop_id", "id"])になります。従来は record[key] がそのまま呼び出され、配列全体を属性名として検索するため常に nil が返り、全レコードが同一の nil グループに収束し最終的に [] が返されました。この挙動は例外を投げずに結果が消失するため、バグの発見が困難でした。
技術的な変更
activerecord/lib/active_record/disable_joins_association_relation.rb の load メソッド内で キーが配列かどうかを判定し、配列の場合は各カラムを個別に取得して配列に変換するロジックを追加しました。変更前は record[key] だけでしたが、変更後は次のようになります。
records_by_id = records.group_by do |record|
if key.is_a?(Array)
key.map { |column| record[column] }
else
record[key]
end
end
この修正により、複合キーでも正しく [shop_id, id] のタプルが取得され、期待通りのグルーピングが行われます。加えて、activerecord/test/cases/associations/has_many_through_disable_joins_associations_test.rb に test_ordered_disable_joins_through_with_composite_primary_key_source を追加し、Cpk::Author, Cpk::Book, Cpk::Order を用いた実際のシナリオで no_joins_orders が正しい結果を返すことを検証しています。テストは SQLite3 と PostgreSQL の両方で緑化され、既存の 29 + 5 + 173 + 320 のテストもすべて合格しています。
設計判断
複合キーの処理は 既存の load ロジック内に最小限の分岐として実装され、他のコードパスやインターフェースは変更されていません。新たにキー型を判定するだけなので、後方互換性が保たれ、disable_joins: true を従来通り true のみで使用しているアプリケーションに影響はありません。また、別途 disable_joins 用のヘルパーメソッドやオプションを増やすことなく、一箇所でのロジック追加で全体の挙動を統一できる設計となっています。
まとめ
この PR は disable_joins:true で複合主キーを持つモデルを通過する has_many :through/has_one :through の結果が空になるバグを解消し、既存の設定やコードに影響を与えずに正しいレコード取得を保証します。これにより、disable_joins を安全に活用できる範囲が広がり、パフォーマンス最適化の選択肢が増えました。