【Rails】検索結果をテーブルに表示し、かつそのテーブルをカラムによってソート可能でページネーションにも対応させる

# はじめに

この記事は、以下のやりたいことを実現するための方法を紹介しています。

> 検索結果をテーブルに表示し、
> かつそのテーブルをカラムによってソート可能で
> ページネーションにも対応させること

# 実装方法
## Gemfile

ページネーションは[will_paginate](https://github.com/mislav/will_paginate) gemを使います。Gemfileに以下を追記し、bundle installしてください。

```ruby:Gemfile
gem "will_paginate", "~> 3.1.7"
```

## Helper

以下のヘルパーを作ります。このヘルパーは[こちらの記事](https://qiita.com/DURIAN_JADE/items/61fd692f04600a6cf9ff)で紹介されているものをベースに、検索パラメータを付けてSortリクエストできるようにしています。

```ruby:app/helpers/search_helper.rb
module SearchHelper

  def sort_asc(column_to_be_sorted, search_params = nil)
    opts = { :column => column_to_be_sorted, :direction => "asc" }
    opts.merge!(search_params) if search_params.present?
    link_to "▲", opts
  end

  def sort_desc(column_to_be_sorted, search_params = nil)
    opts = { :column => column_to_be_sorted, :direction => "desc" }
    opts.merge!(search_params) if search_params.present?
    link_to "▼", opts
  end

  def sort_direction
    %W[asc desc].include?(params[:direction]) ? params[:direction] : "asc"
  end
end
```

## View

以下は適当な検索フォームとテーブルを実装したViewです。
例は、著者と記事の1対多の関係で、記事一覧を表示するものです。ちなみに、スタイルは[Bootstrap](https://getbootstrap.com/)を使用していますが、本題とは関係ありません。

```erb:app/views/articles/index.html.erb
<div class="container">
  <!-- 検索フォーム -->
  <%= form_with url: articles_path, method: :get, local: true do |form| %>
    <div class="row mb-3">
      <div class="input-group col">
        <div class="input-group-prepend">
          <span class="input-group-text">タイトル</span>
        </div>
        <%= form.text_field :title, class: "form-control", value: @search_params[:title] %>
      </div>
      <div class="input-group col">
        <div class="input-group-prepend">
          <span class="input-group-text">著者名</span>
        </div>
        <%= form.text_field :author, class: "form-control", value: @search_params[:author] %>
      </div>
    </div>
    <div class="row mb-3">
      <div class="input-group col">
        <div class="input-group-prepend">
          <span class="input-group-text">発刊年</span>
        </div>
        <%= form.text_field :publication_year_from, class: "form-control", 
                                                    value: @search_params[:publication_year_from] %>
        <div class="input-group-prepend">
          <span class="input-group-text">~</span>
        </div>
        <%= form.text_field :publication_year_to, class: "form-control", 
                                                  value: @search_params[:publication_year_to] %>
      </div>
    </div>
    <div class="form-group row">
      <div class="col">
        <%= form.submit '検索', class: 'btn btn-primary pull-right' %>
      </div>
    </div>
  <% end %>

  <!-- テーブル -->
  <%= will_paginate @articles %>
  <table class="table">
    <thead>
    <tr>
      <th scope="col">ID
        <%= sort_asc('id', @search_params) %>
        <%= sort_desc('id', @search_params) %>
      </th>
      <th scope="col">タイトル
        <%= sort_asc('title', @search_params) %>
        <%= sort_desc('title', @search_params) %>
      </th>
      <th scope="col">著者名
        <%= sort_asc('authors.name', @search_params) %>
        <%= sort_desc('authors.name', @search_params) %>
      </th>
      <th scope="col">発刊年
        <%= sort_asc('publication_year', @search_params) %>
        <%= sort_desc('publication_year', @search_params) %>
      </th>
    </tr>
    </thead>
    <tbody>
    <% @articles.each do |it| %>
      <tr>
        <th scope="row"><%= it.id %></th>
        <td><%= it.title %></td>
        <td><%= it.author.name %></td>
        <td><%= it.publication_year %></td>
      </tr>
    <% end %>
    </tbody>
  </table>
</div>
```

`sort_asc`, `sort_desc`メソッドの第1引数には、ソートのキーにしたいカラム名を指定します。著者名だけ少し違いますが、理由は後述します。

## Controller

Articleモデルを`order`, `paginate`, `search`のメソッドチェーンにより絞り込んだものを`@articles`変数に格納しています。`search`メソッドはArticleモデルに定義した検索メソッドです。

```ruby:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  include SearchHelper

  def index
    sort_column    = params[:column].presence || 'id'
    @articles      = Article.joins(:author).search(search_params)
                            .order(sort_column + ' ' + sort_direction)
                            .paginate(page: params[:page], per_page: 10)
    @search_params = search_params
  end

   def search_params
    params.permit(:title, :author, :publication_year_from, :publication_year_to)
  end
end
```

Articleに関連するAuthorの`name`でソートしたい場合は、

```ruby
Article.joins(:author).order('authors.name asc')
```

とすることで可能です。
Viewの著者名でソートする部分で`sort_asc`, `sort_desc`メソッドに`authors.name`を指定しているのはこのためです。

## Model

Articleモデルに`search`メソッドを生やしています。検索パラメータを引数に受け取り、任意の検索条件によってフィルタしたものを返します。

```ruby:app/models/article.rb
class Article < ApplicationRecord
  belongs_to :author

  def self.search(params)
    title       = params[:title]
    author_name = params[:author]
    year_from   = params[:publication_year_from]
    year_to     = params[:publication_year_to]
    @articles   = Article.all
    if author_name.present?
      author = Author.find_by(name: author_name)
      @articles = @articles.where(author_id: author.id) if author.present?
    end
    @articles = @articles.where("publication_year >= ?", year_from) if year_from.present?
    @articles = @articles.where("publication_year <= ?", year_to) if year_to.present?
    @articles
  end
end
```

```ruby:app/models/author.rb
class Author < ApplicationRecord
  has_many :articles
end
```

# まとめ
上記方法により、検索結果を一覧表示し、ソートも可能でページネーションにも対応しているテーブルができます。

### 参考
[RailsでテーブルのSortかつPaginationを実現できた](https://qiita.com/DURIAN_JADE/items/61fd692f04600a6cf9ff)