Rails ActiveRecordでgroup_by countによる集計結果をrelationとして取得する


先に結論だけ

countメソッドを利用するとhashが返却されるため
selectメソッド内でmysqlのcount処理を記述

articles =
  Article
    .joins(:comments)
    .select('articles.id, articles.category_id, count(comments.id) as comments_count')
    .group(:id)
    .order('comments_count desc')

はじめに

RailsのActiveRecordを通してmysqlでcountした結果を取得しようと思いました。
その際、countメソッドを利用すると返却値がhashとなってしまったため
ActiveRecord::relationで取得する方法を模索します。

前提

ブログサイトをイメージして、以下のようなデータモデルを想定します。

データモデル

各テーブルの説明

記事(articles)

ブログ記事を表します。

カテゴリ(categories)

記事に付与されるカテゴリです。
この投稿記事だとRailsのようなものです。
モデルをシンプルにするために、記事はカテゴリを一つだけ持つことにします。

コメント(comments)

記事に付与されるコメントです。
一つの記事に対して複数のコメントがひもづきます。

データ例

categories

id name
1 音楽
2 プログラミング
3 旅行

articles

id category_id title body
1 1 6月に言ったライブ ...
2 2 rubyについて思うこと ...
3 1 6月に聞いた新譜一覧 ...
4 2 sidekiqを触ってみた ...
5 2 enumerableを理解する ...
6 3 夏休みフランス旅行 ...

comments

id article_id body user_id
1 1 いいね 1
2 2 なるほど 2
3 1 面白い 4
4 4 深いなぁ 5
5 3 うける 9
6 3 ナイス 10
7 1 勉強になる 8

今回取得したいデータ

概要

ActiveRecordを通じて 各記事ごとのコメント数ランキング を取得します。
また、その際に

  • 記事タイトル(articles.title)
  • カテゴリ名(categories.name)

も取得します。

必要データ

id category_name title comment_count
1 音楽 6月に言ったライブ 3
3 音楽 6月に聞いた新譜一覧 2
2 プログラミング rubyについて思うこと 1
4 プログラミング sidekiqを触ってみた 1

こんなイメージです。

まずは: count・sizeメソッドを使う

ActiveRecordを通じてcount文を発行するメソッドに countやsizeがあります。(どちらを使うべきかは他の記事で多々挙げられているので、そちらを参考のこと)
今回はcountメソッドを用いて集計をしてみます。

counts = 
  Article
    .joins(:comments)
    .group(:id)
    .order('count_all desc')
    .count

発行されるクエリは以下の通りです。

SELECT  
  COUNT(*) AS count_all, 
  `articles`.`id` AS articles_id 
FROM `articles` 
INNER JOIN `comments` 
  ON `comments`.`article_id` = `articles`.`id` 
GROUP BY `articles`.`id` 
ORDER BY count_all desc

返却されるデータは以下の通りです。

{ 1=>3, 3=>2, 2=>1, 4=>1 }

ActiveRecord::relationではなくhashが返却されてしまったため、
記事タイトルおよびカテゴリ名を取得するには、
記事リレーションを取得するクエリを再発行することが必要になります。

articles = Article.find(counts.map{|id, count| id})

つぎに: selectメソッドを使う

あまり綺麗な方法ではないですが、
countメソッドを使う代わりにselectメソッドを用いて
明示的にselect文内で集約処理を実現します。

articles =
  Article
    .joins(:comments)
    .select('articles.id, articles.category_id, count(comments.id) as comments_count')
    .group(:id)
    .order('comments_count desc')

発行されるクエリは以下の通りです。

SELECT  
  articles.id, 
  articles.category_id, 
  count(comments.id) as comments_count 
FROM `articles` 
INNER JOIN `comments` 
  ON `comments`.`article_id` = `articles`.`id` 
GROUP BY `articles`.`id` 
ORDER BY comments_count desc

返却されるデータは以下の通りです。

[
  #<Article: id: 1, category_id: 1>,
  #<Article: id: 3, category_id: 1>,
  #<Article: id: 2, category_id: 2>,
  #<Article: id: 4, category_id: 2>
]

Articleモデルのrelationが返ってきました。

コメント数を取得する場合は

articles[0].comments_count
=> 3

カテゴリ名を取得する場合は

articles[0].category.name
=> "音楽"

relationとして取得することができるようになりました。