大阪でRailsを中心に学習している薬剤師エンジニア(初学者)こと、ヨマ(@yoma_2003)です!
Railsで子モデルの最新レコード
を基準に親モデルをソート
する方法をまとめます。
※おことわり※
断定口調でまとめますが、初学者であるため間違い等あればご指摘頂けると嬉しいです。
はじめに
Railsで次のような親子関連を持ったモデルがあったとします。
class Character < Application Record
has_many :topics
end
class Topic < ApplicationRecord
belongs_to :character
end
charactersテーブル
id | name | describe |
---|---|---|
1 | 田中 | 初学者 |
2 | 鈴木 | Rails学習中 |
3 | 佐藤 | サウナ好き |
topicsテーブル
id | content | created_date | character_id |
---|---|---|---|
1 | joinsメソッドにてついて | 2022-07-01 | 2 |
2 | whereメソッドについて | 2022-07-04★ | 2 |
3 | Rubyとは | 2022-06-28 | 1 |
4 | Qiitaとは | 2022-07-05★ | 1 |
5 | おすすめのサウナ | 2022-07-07★ | 3 |
6 | サウナの入り方 | 2022-07-03 | 3 |
※それぞれのキャラクターの最新トピックに★をつけています。
やりたいこと
キャラクターそれぞれの最新トピックが新しい順にキャラクターを並べ替えたい
具体的には、子モデルであるtopics
テーブルのcreated_date
カラムが最新のものを基準に、親モデルのcharacters
テーブルをソートしたい。
取得したいcharactersテーブル
id | name | describe |
---|---|---|
3 | 佐藤 | サウナ好き |
1 | 田中 | 初学者 |
2 | 鈴木 | Rails学習中 |
ちなみに、update_at
(一意な値といえる)などで取得する方法もありましたが、子モデルに日付編集機能を導入したくcreated_date
という自作のカラム(重複することあり)でもソートできる方法考え少しハマったので、その方法をまとめてみました。
結論
AcrtiveRecordのjoins
メソッドやorder
メソッドの使用も考えましたが、
今回は、シンプルに生のSQLを読み込めるActiveRecordのfind_by_sql
メソッドを用いてソート機能を実装してみました。
class Character < Application Record
has_many :topics
scope :sort_new, -> {find_by_sql(
"SELECT c.*, MAX(t.created_date)
FROM characters c
INNER JOIN topics t ON t.character_id = c.id
GROUP BY t.character_id
ORDER BY max(t.created_date) DESC"
)}
end
class CharactersController < ApplicationController
def index
@characters = Character.sort_new
end
・・・
end
解説
SQL以外の部分
scope :sort_new, -> {find_by_sql("SQL文")}
@characters = Character.sort_new
-
scope(スコープ)
とは、クラスメソッドを使う際、可読性を保つためにあるものです。
上記の様にfind_by_sqlなどの記述が長くなりそうなクラスメソッドをコントローラーに書かず、モデルに新たなクラスメソッドとして定義することで、コントローラーでは簡潔な記述で処理を呼び出すことができます。
-
-> (ラムダ)
とは、続くブロックをオブジェクト化してProcと呼ばれるオブジェクトにするものです。
SQL部分
SELECT c.*, MAX(t.created_date) --③t.create_dateの最大値とcharactersテーブルの全カラムを取得
FROM characters c
INNER JOIN topics t ON t.character_id = c.id --①charactersテーブルとtopicsテーブルを結合
GROUP BY t.character_id --②t.character_idでグループ化
ORDER BY max(t.created_date) DESC --④t.created_dateの最大値でソート
SQL評価順序を考えて記述し、結合したテーブルからレコードを絞り、ソートします。
結合したテーブル
id | name | describe | id | content | created_date | character_id |
---|---|---|---|---|---|---|
1 | 田中 | 初学者 | 3 | Rubyとは | 2022-06-28 | 1 |
1 | 田中 | 初学者 | 4 | Qiitaとは | 2022-07-05★ | 1 |
2 | 鈴木 | Rails学習中 | 1 | joinsメソッドにてついて | 2022-07-01 | 2 |
2 | 鈴木 | Rails学習中 | 2 | whereメソッドについて | 2022-07-04★ | 2 |
3 | 佐藤 | サウナ好き | 5 | おすすめのサウナ | 2022-07-07★ | 3 |
3 | 佐藤 | サウナ好き | 6 | サウナの入り方 | 2022-07-03 | 3 |
※それぞれのキャラクターの最新トピックに★をつけています。created_at、update_atカラムは省略。
- ポイントは
GROUP BY
でまとめたレコードにMAX
を使用することで、グループごとのcreate_dateの最大値を取得し、この最大値で最後にソートを実行している点です。
最終的にSQLではMAX(t.create_date)
というカラムも出力されますが、find_by_sql
メソッドではモデルに定義されたカラムしか返さないので必要なcharactersの全カラムのみ
が返され、ソートされたcharacterインスタンス群
が取得できます。
結果、ビューでは以下の順でレコードが表示されます。
取得できた@characters
id | name | describe |
---|---|---|
3 | 佐藤 | サウナ好き |
1 | 田中 | 初学者 |
2 | 鈴木 | Rails学習中 |
おわりに
私はMySQLを使用しておりますが、以下のページに、データベースの種類によってはGROUP BYの挙動が違うという情報がありました。
未確認ですが、気になる方は参考にして下さい。
PostgreSQLの場合Group ByではSELECTで登場する関数以外をGroup byに書く必要があります。MySQL, SQLliteでは全て書く必要はない。