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

大阪でRailsを中心に学習している薬剤師エンジニア（初学者）こと、ヨマ([@yoma_2003](https://twitter.com/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と呼ばれるオブジェクトにするものです。

https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3

https://shinmedia20.com/rails-scope-lambda#lambda

https://shinmedia20.com/ruby-proc#Proc-2

### SQL部分
```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学習中 |


https://qiita.com/suzukito/items/edcd00e680186f2930a8

# おわりに
私はMySQLを使用しておりますが、以下のページに、データベースの種類によってはGROUP BYの挙動が違うという情報がありました。
未確認ですが、気になる方は参考にして下さい。
> PostgreSQLの場合Group ByではSELECTで登場する関数以外をGroup byに書く必要があります。MySQL, SQLliteでは全て書く必要はない。

https://programmer-life.work/sql/max-sql#Max