（Rails, Ransack）リレーション先のテーブルのカラムをソートする

# やりたいこと
Railsのgem、[ransack](https://github.com/activerecord-hackery/ransack)で検索やソートができますが、リレーション先のレコードをソートしたいです。

例えば、`posts`テーブルと`users`テーブルがあるとします。
`posts`のビュー `index.html.erb`で、postを投稿したユーザーも表示しソートできるようにしたいです。

各モデルは以下のような感じ（必要なとこだけ抜粋しています）。

```ruby:post.rb
# == Schema Information
#
# Table name: posts
#
#  id         :bigint           not null, primary key
#  user_id    :bigint           not null
#  body       :text             not null

belong_to :user
```
```ruby:user.rb
# == Schema Information
#
# Table name: users
#
#  id               :bigint           not null, primary key
#  name_kanji       :text             not null
#  name_kana        :text             not null

has_many :posts
```

ユーザーのカラムには、漢字氏名の他、読み仮名も持たせています。


### 環境
ruby 2.7.2
Rails 6.1.3.1

# 実装！

## View

```erb:posts/index.hrml.erb
<%= sort_link(@q, :user_name_kana, 'ユーザー') %>
```
- sort_linkの第二引数に`:name`と書きたくなってしまいますが、ユーザーのnameというカラムを持っているのはpost.rbではなく、user.rbなので思った通りの挙動にならないはずです。ここでは`user_name_kana`というscopeを使います。これは次にModelで定義します。


## Model
postモデルでscopeを作ります。
今回の本題ではないのですが、ransackさんは日本語のソートがあまり得意ではないようので、下のように生のクエリを書くような実装となります。

```ruby:post.rb
  scope :sort_by_user_name_kana_asc, lambda {
    eager_load(:user)
      .order(Arel.sql('users.name_kana COLLATE "C" ASC'))
  }

  scope :sort_by_user_name_kana_desc, lambda {
    eager_load(:user)
      .order(Arel.sql('users.name_kana COLLATE "C" DESC'))
  }
```

いくつかポイントを列挙します。

- scopeメソッドは特定のSQL文に名前を付けてメソッド化できるメソッドです。第一引数に付けたいメソッド名、第二引数にlambdaで処理を記述します。

- ransackはこのscopeで`:sort_by_任意のカラム名_asc` `:sort_by_任意のカラム名_desc`と書いてあげることで複雑なソートも対応してくれます。ここでは、この`任意のカラム名`に先ほどビューで書いた`user_name_kana`を使います。（参考：Ransack GitHub [Ransack's sort_link helper creates table headers that are sortable links](https://github.com/activerecord-hackery/ransack#ransacks-sort_link-helper-creates-table-headers-that-are-sortable-links)）
- `name_kana`の前に`users.`と付加することで、`ORDER BY posts.name_kana`ではなく`ORDER BY users.name_kana`というクエリが生成され、１対多で関連付いている別のモデルのカラムをSQLで操作することができます。
- eager_loadとはRailsのORMであるActive Recordのメソッドで、生のSQLを書くことなく、クエリを実行できるものです。
- ~~このeager_loadしてあげないとSQLがびっくりしてしまいます。「うちPostやけん、usersテーブルとか知らんし」と。なので`eager_load`して根回ししておきます。「お宅の関連会社のuserさんおられますわな？そこのデータ見てきてもろてよろしいでしょうか？」的な。~~
- （追記）↑ずいぶんと頭の悪そうな説明を書いてしまいましたが、要はLEFT OUTER JOINのSQLを発行するメソッド見たいですね。。https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/QueryMethods.html#method-i-eager_load

### 実際にSQLを見てみる
eager_loadを入れないのと入れるのとでクエリがどう変わるのか、`rails c`してどんなクエリが実際走るのか見てみます。
#### eager_loadを入れないと

```ruby
pry(main)> Post.sort_by_user_name_kana_asc.to_a
```

こんなSQLが走ります。（見やすいように改行で整形しています）

```sql
Post Load (1.6ms)
SELECT "posts".* FROM "posts"
ORDER BY users.name_kana COLLATE "C" ASC
```

こんなエラー文が出ました。

```
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "users"
LINE 1: ...ELECT "posts".* FROM "posts" ORDER BY users.f...
```
根回ししていないので、usersテーブルとか知らんしと怒っています。

#### eager_loadを入れると

```ruby
pry(main)> Post.eager_load(:user).sort_by_user_name_kana_asc.to_a
```

こんなSQLが走ります。（見やすいように改行で整形しています。それと、AS以降は参照しないでください。）

```sql
SQL (3.5ms)
SELECT "posts"."id" AS t0_r0,
"posts"."user_id" AS t0_r1,
"users"."id" AS t1_r0,
"users"."name" AS t1_r2,
"users"."name_kana" AS t1_r3, 
LEFT OUTER JOIN "users" ON "users"."id" = "posts"."user_id"
ORDER BY users.name_kana COLLATE "C" ASC
```

ポイントは`LEFT OUTER JOIN`です。これによりusersテーブルをJOINし、スキャンすることに成功しました。


# 最後に
まとめ

- リレーション先の他のモデルのカラムをソートしたければ、scopeで定義してあげる。
- eager_loadでリレーション先のDBをJOINすることを忘れないこと。

何かご指摘などあればバシバシよろしくお願いします。

[@yuuu](https://qiita.com/Y_uuu)先輩に感謝しつつ。