Ruby on Rails の Active Record って便利ですよね。
SQL から解放されたかのようにデータアクセスできるので、心穏やかに DB と向き合えます。
でも、以下のような SQL をイメージしてデータを取得したい場合はどうでしょうか。
SELECT
users.*,
COUNT(thanks.id) AS thanks_count
FROM
users
LEFT OUTER JOIN
thanks
ON users.id = thanks.user_id
GROUP BY
users.id
;
users
と thanks
は親子関係のテーブルで、 1:n の関係性を持っているケースです。
users
に紐づく thanks
の件数を取得したいと考え、例えば以下のようなコードを書いてしまうと、よく聞く n+1 問題が発生してしまいます。
User.all.thanks.each { |thank| thank.count }
この対処法としてよく聞くのが eager_load
や joins
などです。
eager_load
とか joins
とか includes
とか、いろいろあって何がどうだったかよく分からなくなっちゃいますよね。
それぞれの特徴に関して、私はよく以下の記事を参考にしております。お世話になっております。
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
結論としては、結合先のテーブルで COUNT をしたい場合は joins
が有効です。
eager_load
では、結合先テーブルの全カラムを select に入れてしまい、集計関数が使えなくなってしまうためです。
なので、上記の SQL 的なデータアクセスをする場合は、以下のように書きます。
User.all.left_joins(:thanks).group(:id).select('users.*, COUNT(`thanks`.`id`) AS thanks_count')
もし、これを複数箇所で使う場合、さらに他のテーブルなどにも汎用性をもたせたい場合、以下のように Model にメソッド化しておきたいですね。
class User < ActiveRecord::Base
scope :add_count_column, -> (join_table_name) do
scope = current_scope || relation
scope = scope.select("`#{table_name}`.#{Arel.star}") if scope.select_values.blank?
scope.left_joins(join_table_name).group(:id).select('COUNT(`#{join_table_name}`.`id`) AS #{join_table_name}_count')
end
end
上記の書き方は、以下の記事を参考にさせていただきました。ありがとうございます。
Rails: SELECTするカラムを追加するscopeを定義する
おわりに
Ruby on Rails の Active Record は良いツールではあるのですが、それだけで完結できないこともあります。
どこかで SQL を考え、パフォーマンスを意識しておかないといけないのかもしれません。
今回紹介した joins
や、eager_load
、includes
を使いこなせると、こうした問題に立ち向かえる力が増すと思いますよ。