LoginSignup
12
7

More than 5 years have passed since last update.

Ruby on Rails の ActiveRecord で結合先のテーブルを COUNT する場合は joins/left_joins が良い

Last updated at Posted at 2018-07-04

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
;

usersthanks は親子関係のテーブルで、 1:n の関係性を持っているケースです。

users に紐づく thanks の件数を取得したいと考え、例えば以下のようなコードを書いてしまうと、よく聞く n+1 問題が発生してしまいます。

User.all.thanks.each { |thank| thank.count }

この対処法としてよく聞くのが eager_loadjoins などです。

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_loadincludes を使いこなせると、こうした問題に立ち向かえる力が増すと思いますよ。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7