[Rails]ransackでセレクトボックスを使ってソートする

プログラミングスクールの課題で某フリマサイトのコピーサイトを作っていた時のこと。ransackを使って検索フォームを作っていたのですが、以下のようなセレクトボックスを使ったソート機能の作り方が調べてもヒットしませんでした。（ransackのソート機能といえばsort_linkがあるがちょっと違うし・・・）
そこで、この機能を自分なりに実装してみたので紹介します。

![31045d73bf0b3c2ecdedbf2ac70177d6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400652/d2d21bcf-9a77-46fb-c245-9322a69da793.gif)

## いいね!の多い順でのソートを実装しない場合
いいね!の多い順でソートする方法は少し複雑なので先にいいね!以外のパラメーターでソートする方法を説明します。

### モデル

今回はItemモデルのオブジェクトをソートします。
itemにはpriceカラムがあるとします。

```ruby
  def change
    create_table :items do |t|
      t.integer :price, null: false
      t.timestamps
    end
  end
```

### ルーティング
今回はPOSTで検索フォームを送信します。また、検索フォーム以外からのアクセスはGETになるため、GETでもPOSTでも表示できるようにルーティングを設定します。
今回はitemsコントローラーのsearchアクションとしました。

```ruby:routes.rb
  resources :items do
    collection do
      match 'search' => 'items#search', via: [:get, :post]
    end
  end
```

### ビュー
検索オブジェクト(今回は@search)のsortsにソート条件を入れてあげればいいので、selectを以下のようにします。ビューの作成にはHamlを採用しています。

```ruby:search.html.haml
  = search_form_for @search, url: search_items_path ,html: { method: :post } do |f|
    = f.select( :sorts, { '並び替え': 'id desc', '価格の安い順': 'price asc', '価格の高い順': 'price desc', '出品の古い順': 'updated_at asc', '出品の新しい順': 'updated_at desc' } , { selected: params[:q][:sorts] }, { onchange: 'this.form.submit()'} )
  # 適宜、検索条件を追加してください
  = f.submit '完了'
```

keyがビューに表示され、valueが@search.sortsに入ります。今回は価格と出品日時でソートしたいので、itemsテーブルにあるpriceとupdated_atでソート条件を指定します。

selectedにparams[:q][:sorts]を指定することで、前回の検索条件を表示することができます。さらに、onchange: 'this.form.submit()'を記述することで、セレクトボックスが選択された時点でフォームを送信します。

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

```ruby:items_controller.rb
  def search
    if params[:q].present?
    # 検索フォームからアクセスした時の処理
      @search = Item.ransack(search_params)
      @items = @search.result
    else
    # 検索フォーム以外からアクセスした時の処理
      params[:q] = { sorts: 'id desc' }
      @search = Item.ransack()
      @items = Item.all
    end
  end

  def search_params
    params.require(:q).permit(:sorts)
    # 他のパラメーターもここに入れる
  end
```

ビューでselectedオプションを使って検索条件を引き継いでいるので、params[:q][:sorts]に値が入っていないとエラーになります。そのため、else以下では`id desc`をデフォルトのソート条件として代入しています。

これで、セレクトボックスを使ったソートができるようになりました。
## いいね!の多い順にソートする場合
いいね!の多い順でソートする場合は、あるitemに紐づいているlikesのレコード数をカウントしてソートする必要があります。
これはransackのデフォルトの機能にはないので（間違ってたらごめんなさい）、ransackerという拡張機能を使ってソート条件を自作する必要があります。

### モデル
以下のようなアソシエーションを定義します。

```ruby:item.rb
  has_many :likes
```

```ruby:like.rb
  belongs_to :item
```
itemから見ると、itemとlikeにはhas_manyの関係があります。
(いいね!の送り元のUserモデルなどは適宜作成してください)
そして、likesテーブルには以下のようなカラムがあります。

```ruby
  def change
    create_table :likes do |t|
      t.references :user, null: false, foreign_key: true
      t.references :item, null: false, foreign_key: true
      t.timestamps
    end
  end
```

Itemモデルに以下を追記します。

```ruby:item.rb
  ransacker :likes_count do
    query = '(SELECT COUNT(likes.item_id) FROM likes where likes.item_id = items.id GROUP BY likes.item_id)'
    Arel.sql(query)
  end
```
今回はlikesのカウントでソートする`likes_count`という条件を作っています。ransackでは内部的にArelを使用しているので、`query`にSQLを入れてそのあとにArelに変換しています。
これで、`likes_count`が使えるようになりました。

### ビュー
作成した`likes_count`をセレクトボックスに追加します。

```ruby:search.html.haml
  = search_form_for @search, url: search_items_path ,html: { method: :post } do |f|
    = f.select( :sorts, { '並び替え': 'id desc', '価格の安い順': 'price asc', '価格の高い順': 'price desc', '出品の古い順': 'updated_at asc', '出品の新しい順': 'updated_at desc', 'いいね!の多い順': 'likes_count desc' } , { selected: params[:q][:sorts] }, { onchange: 'this.form.submit()'} )
  = f.submit '完了'
```

これで、いいね!の多い順にソートすることができるようになりました。

## 終わりに
以上でセレクトボックスを使ったソート方法の紹介を終わります。
今回はビューに選択肢を書いておりファットな表現になってしまっているので別の場所に記述したほうがいいのかなと思いましたが、Rails初心者なためどこに移せばいいかわかりませんでした。
詳しい方教えてください。他にもおかしなところがあれば指摘いただければと思います。
[追記]
あまり選択肢が多くなるのであれば[active_hash](https://github.com/zilkey/active_hash)を使うのが良さそうですね

## 参考
[activerecord-hackery/ransack](https://github.com/activerecord-hackery/ransack)
[Ransackで簡単に検索フォームを作る73のレシピ](http://nekorails.hatenablog.com/entry/2017/05/31/173925)
[Ransack](https://srcw.net/wiki/?Ransack)
[[Rails]ransackを利用した色々な検索フォーム作成方法まとめ](https://qiita.com/nishina555/items/2c1f8bae980e426519bc)