読者です 読者をやめる 読者になる 読者になる

ぽかぽかウンティの自由帳

ぽかぽか動物園から投稿します。

How we can join multiple tables, aggregate multiple columns, and sort by its aggregated columns on the Rails ActiveRecord?

RailsActiveRecord で複数テーブルを結合して複数カラムで集計して、その集計結果でソートしたい場合のレールの乗り方がわからなかったので色々と試した結果、疲れた。

例えば以下みたいな普通の SQLActiveRecord::Relation で組み立てて、かつ ActiveRecord::Base を継承したモデルや便利な Gem のインターフェースを使う場合の話。

/* student has many scores */
SELECT
  students.*,
  AVG(scores.math) AS avg_math_score,
  AVG(scores.english) AS avg_english_score
FROM
  students
    INNER JOIN scores
      ON students.id = scores.student_id
WHERE
  students.id IN (1, 2, 3)
GROUP BY
  students.id
ORDER BY
  avg_math_score DESC

github.com
github.com

結論から言うと eager_load や preload や includes などの Association cache 機構には頼れなかった。そして Association での Draper gem の利用を諦めた。

Students
  .select(students.*)
  .select('AVG(scores.math) AS avg_math_score')
  .select('AVG(scores.english) AS avg_english_score')
  .joins('LEFT OUTER JOIN scores ON students.id = scores.student_id')
  .group('students.id')
  .order('avg_math_score DESC')

で Student モデルに avg_math_score や avg_english_score のメソッドが生えた形で結果配列を取得できるようになる。Kaminari Gem のページネーションはそのまま使える。しかし N+1 クエリを防ぐためには Association での Draper Gem は諦めざるを得なかった。

Rails5 だと left_joins というクエリインターフェースが追加されているみたいなので、バージョン上げたら試してみようと思う。

qiita.com

includes は内部で条件に応じて eager_load や preload の挙動となる。

qiita.com

preload は絞り込み条件が付くとエラーになるので今回の要件ではそもそも使えない。

eager_load だと内部で t0_r0 みたいに列名を管理してモデルオブジェクトをキャッシュしているみたいで、その過程で独自に select で追加した集計結果列が捨てられてしまっていた。さらに内部で意図しない壊れたカウントクエリが発行されてしまいエラーになった。

前提として arel とかは個人的には手を出したくない。もっといい方法がないか引き続き探ってみたい。