【ransack】関連するモデルのレコード数による並び替え

# 概要
ransackで関連するモデルのレコード数による並び替えを実装した時のことを備忘録として記録します。

# 環境
・ruby '2.5.7'
・rails '5.2.3'

# 前提
・gem 'ransack'は導入済であること

# 過程
## 0.自モデル内での並び替え
まずは、**関連するモデルのレコード数による並び替え**を実装する前に、自モデル内での並び替えを実装していきます。

### 対象のモデル
今回は、Datespotモデルでの並び替えを実装していきます。
Datespotモデルには、`dateカラム`があるとします。

```（抜粋）db/schema.rb
  create_table "datespots", force: :cascade do |t|
    t.integer "user_id", null: false
    t.datetime "date"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
```
### ビュー
検索オブジェクトの`sorts`に並び替え条件を入れてあげればいいので、`select`を以下のようにします。

```ruby:datespot_search.html.erb
<%= search_form_for @search do |form| %>
  <div class='datespot-search__search-form'>　# class名は、適宜変更してください
  　# 検索条件は、適宜追加してください
    <%= form.select( :sorts, {'予定日が近い順': 'date asc', '予定日が遠い順': 'date desc', '更新日が新しい順': 'updated_at desc', '更新日が古い順': 'updated_at asc'}, { selected: params[:q][:sorts] }, class: 'datespot-search__search-form--sort-field' ) %>
    <button type="submit" class="datespot-search__search-form--search-button">
      <i class="fas fa-search datespot-search__search-form--search-icon"></i>
    </button>
  </div>
<% end %>
```

`key`がビューに表示され、`value`が`@search.sorts`に入ります。
今回は予定日と更新日でソートしたいので、Datespotモデルにある`dateカラム`と`updated_atカラム`でソート条件を指定しています。

さらに、`selected`に`params[:q][:sorts]`を指定することで、検索条件が残すことができます。

### コントローラー
コントローラーではフォームから受け取った`params[:q]`を使って検索と並び替えを実行します。

```ruby:app/controllers/datespots_controller.rb
  def index
    # 検索フォームからアクセスした時の処理
    if params[:q].present?
      @search = Datespot.ransack(params[:q])
      @datespots = @search.result
    else
    # 検索フォーム以外からアクセスした時の処理（デフォルトの並び順）
      params[:q] = { sorts: 'date asc' }
      @search = Datespot.ransack(params[:q])
      @datespots = @search.result
    end
  end
```

ビューで`selected`オプションを使って、検索条件を引き継いでいるので、
`params[:q][:sorts]`に値が入っていないとエラーになります。
そのため、else以下では`date asc`をデフォルトの並び替え条件として代入しています。
ここは、任意で設定してください。

これで、自モデル内での並び替えを実装できました！！

## 1.関連するモデルのレコード数による並び替え
それでは、**関連するモデルのレコード数による並び替え**を実装していきます。

### 対象のモデル
今回は、Datespotモデルと1対多のアソシエーションがあるCommentモデルでの並び替えを実装していきます。
Commentモデルには、以下のようなカラムがあるとします。

```（抜粋）db/schema.rb
  create_table "comments", force: :cascade do |t|
    t.integer "user_id", null: false
    t.integer "datespot_id", null: false
    t.text "content", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
```

### 実装したいこと
ここでは、各datespotに紐づく**コメント数**による並び替えを実装していきます。
そのために、関連レコード数を集計する`counter_cache`というRailsの機能を使用します。
`counter_cache`設定することにより、関連するテーブルのレコード数を簡単にカウントできます。

### counter_cacheの追加
DatespotモデルとCommentモデルは以下のようになります。

```ruby:app/models/datespot.rb
class Datespot < ApplicationRecord
  has_many :comments
end
```

```ruby:app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :datespot, counter_cache: :comments_count
end
```
ここで1つ注意が必要です。`counter_cache`はbelongs_to宣言で指定する必要があります。
また、実際に数を数えたいカラムは「相手」のモデル（関連付けられているモデル）の方に追加する必要があります。
上記の例では、Datespotモデルの方に`comments_countカラム`を追加する必要があります。

### comments_countカラムの追加
Datespotモデルに`comments_countカラム`を追加しましょう。

```ruby:db/migrate/2020××××××××××_add_comments_count_to_datespots.rb
class AddCommentsCountToDatespots < ActiveRecord::Migration[5.2]
  def change
    add_column :datespots, :comments_count, :integer, null: false, default: 0
  end
end
```

`rails db:migrate`を忘れずに行います。


### 並び替え条件の追加
あとは、並び替え条件に追加すれば、実装完了です！！

```ruby:datespot_search.html.erb
<%= search_form_for @search do |form| %>
  <div class='datespot-search__search-form'>　# class名は、適宜変更してください
  　# 検索条件は、適宜追加してください
    <%= form.select( :sorts, {（省略）, 'コメント数が多い順': 'comments_count desc'}, { selected: params[:q][:sorts] }, class: 'datespot-search__search-form--sort-field' ) %>
    <button type="submit" class="datespot-search__search-form--search-button">
      <i class="fas fa-search datespot-search__search-form--search-icon"></i>
    </button>
  </div>
<% end %>
```

これで、各datespotに紐づく**コメント数**による並び替えを実装できました！！

## 2.関連するモデルのレコード数による並び替え（応用）
この考えを応用して、各datespotに紐づく**評価の平均値**による並び替えを実装していきます。

### 対象のモデル
同様に、Datespotモデルと1対多のアソシエーションがあるCommentモデルでの並び替えです。
Commentモデルにある`rateカラム`が評価の値をFloatクラス（浮動小数点数）で保存しています。

```（抜粋）db/schema.rb
  create_table "comments", force: :cascade do |t|
    t.integer "user_id", null: false
    t.integer "datespot_id", null: false
    t.text "content", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.float "rate", default: 0.0, null: false # 評価の値を保存している
  end
```

### 実装したいこと
上記したように、各datespotに紐づく**評価の平均値**による並び替えを実装していきます。
そのために、必要な手順を分解すると次のような手順になります。

①　Datespotモデルに`rate_averageカラム`を追加する
②　Commentモデルの`rateカラム`の平均値を取って、Datespotモデルの`rate_averageカラム`を更新する
③　Commentモデルの`rateカラム`が追加される度に、Datespotモデルの`rate_averageカラム`が更新されるようにする
④　並び替え条件の追加

それでは、１つ１つ見ていきましょう！

### ①　Datespotモデルに`rate_averageカラム`を追加する
Datespotモデルに`rate_averageカラム`を追加しましょう。

```ruby:db/migrate/2020××××××××××_add_rate_average_to_datespots.rb
class AddRateAverageToDatespots < ActiveRecord::Migration[5.2]
  def change
    add_column :datespots, :rate_average, :float, default: 0.0
  end
end
```

`rails db:migrate`を忘れずに行います。

### ②　Commentモデルの`rateカラム`の平均値を取って、Datespotモデルの`rate_averageカラム`を更新する
Datespotモデルに`update_rate_avarageメソッド`を定義します。

```ruby:app/models/datespot.rb
  def update_rate_avarage
    comments_average = comments.average(:rate)
    update(rate_average: comments_average)
  end
```

`comments.average(:rate)`で、Commentモデルの`rateカラム`の平均値を取って、それを変数`comments_average`に代入します。

そして、`update(rate_average: comments_average)`で、Datespotモデルの`rate_averageカラム`を更新します。

ちなみに、`update(rate_average: comments_average)`は、`self.update(rate_average: comments_average)`を省略したものです。

### ③　Commentモデルの`rateカラム`が追加される度に、Datespotモデルの`rate_averageカラム`が更新されるようにする
あとは、CommentモデルにActive Recordコールバックを定義します。

```ruby:app/models/comment.rb
  after_save :update_rate_avarage
  after_destroy :update_rate_avarage

  def update_rate_avarage
    datespot.update_rate_avarage
  end
```

今回は、`after_save`と`after_destroy`時に、Datespotモデルの`rate_averageカラム`が更新されるようにしています。

ちなみに、コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。
コールバックを利用することで、Active Recordオブジェクトが作成/保存/更新/削除/検証/データベースからの読み込み、などのイベント発生時に常に実行されるコードを書くことができます。

### ④　並び替え条件の追加
あとは、並び替え条件に追加すれば、実装完了です！！

```ruby:datespot_search.html.erb
<%= search_form_for @search do |form| %>
  <div class='datespot-search__search-form'>　# class名は、適宜変更してください
  　# 検索条件は、適宜追加してください
    <%= form.select( :sorts, {（省略）, '評価が高い順': 'rate_average desc'}, { selected: params[:q][:sorts] }, class: 'datespot-search__search-form--sort-field' ) %>
    <button type="submit" class="datespot-search__search-form--search-button">
      <i class="fas fa-search datespot-search__search-form--search-icon"></i>
    </button>
  </div>
<% end %>
```

これで、各datespotに紐づく**評価の平均値**による並び替えを実装できました！！

# 参考
[[Rails]ransackでセレクトボックスを使ってソートする](https://qiita.com/akkie/items/9d09d21a0e8ee47a1691)
[Railsガイド　Active Record の関連付け](https://railsguides.jp/association_basics.html#belongs-to%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-counter-cache)
[Railsガイド　Active Record コールバック](https://railsguides.jp/active_record_callbacks.html)