Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした