LoginSignup
4
1

More than 1 year has passed since last update.

【Rails】【SQL】子モデルの最新レコードを基準に親モデルをソートする方法

Last updated at Posted at 2022-07-07

大阪でRailsを中心に学習している薬剤師エンジニア(初学者)こと、ヨマ(@yoma_2003)です!
Railsで子モデルの最新レコードを基準に親モデルをソートする方法をまとめます。

※おことわり※
断定口調でまとめますが、初学者であるため間違い等あればご指摘頂けると嬉しいです。

はじめに

Railsで次のような親子関連を持ったモデルがあったとします。

character.rb
class Character < Application Record
  has_many :topics
end
topic.rb
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メソッドを用いてソート機能を実装してみました。

character.rb
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
characters_controller.rb
class CharactersController < ApplicationController
  def index
    @characters = Character.sort_new
  end
  ・・・
end

解説

SQL以外の部分

モデル.rb
scope :sort_new, -> {find_by_sql("SQL文")}
コントローラー.rb
@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では全て書く必要はない。

4
1
8

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
4
1