[Rails]ransackを利用した色々な検索フォーム作成方法まとめ

# はじめに

アプリケーションを作るうえで検索フォームを実装する機会はとても多いと思います。
今回はRails(バージョンは5.0.1)を利用した色々な検索フォームの作成方法をまとめます。前提として`ransack`というgemを利用しています。

記事の構成は以下のようになっています。
まず、サンプルとなる学生検索アプリケーションの作成方法を説明します。次に検索フォームの作成方法の説明をします。最後に検索クエリを表示する方法を説明します。
検索フォームの作成のみを知りたい場合は「[検索フォームの作成](http://qiita.com/nishina555/items/2c1f8bae980e426519bc#%E6%A4%9C%E7%B4%A2%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E4%BD%9C%E6%88%90)」からお読みください。

なお、今回の内容は試行錯誤をしながら作成をしたものなので、もしより良い実装方法などがあれば指摘していただければと思います。

# 今回の記事で説明する内容

今回の記事では以下のような内容について学習をしていきます。

- 1:多、多:多関係のモデルの作成および表示方法
- N+1問題の解決方法
- 色々な検索フォームの作成方法
- 検索クエリの表示方法

## アプリの完成図

今回は以下のような学生検索アプリを作ることをゴールにします。

![search_complete.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/a8c34df4-4736-e5c4-e165-474f17d212a1.gif)

なお、今回のソースコードは[こちら](https://github.com/nishina555/student_search)に置いておきました。

## サンプルデータについて

今回は以下のようなサンプルデータを準備していきます。

| 氏名       | 性別| 年齢 |学部       | 履修科目  |
|:-----------|:---|:----|:---------|:---------|
|鈴木よし子    |女   |  18 | 経済学部  | 数学・社会学|
|伊藤佳穂     |女   |  19 | 外国語学部 | 外国語    |
|鈴木佳孝     |男   |  20 | 社会学部  | 社会学    |
|山田太郎     |男   |  22 | 理工学部  | 数学・外国語|


## ER図

サンプルデータを作るためのER図は以下のようにします。
今回のER図作成には[BurntSushi/erd](https://github.com/BurntSushi/erd)を利用しました。

![student_search-erd.png](https://qiita-image-store.s3.amazonaws.com/0/140792/005d9b4a-df93-195e-7626-4f17c38abeff.png)


ER図からもわかるように、リレーションは以下のようになっています。

- 学生:学部は多:1の関係
- 学生:履修科目は多:多の関係(多:多なので、中間テーブルを用意する)

## 具体的なレコード

各テーブルには以下のようにレコードが入っています。

![スクリーンショット 2017-03-03 1.25.24.png](https://qiita-image-store.s3.amazonaws.com/0/140792/8a8a567f-8eca-e25e-0e14-f3c18332d6e8.png)



# 事前準備

まずは事前準備として学生検索アプリの雛形を作成していきます。

## アプリ作成

まず、アプリケーションの雛形を作成します。

```
$ rails new student_search
$ cd student_search
```

今回利用する`ransack`をGemfileに追加します。

```Gemfile
gem 'ransack'
```

インストールします。

```
$ bundle install --path vendor/bundle
```

## モデルの作成

モデルを作成します。

```
$ bundle exec rails g model department name
$ bundle exec rails g model subject name
$ bundle exec rails g model student name sex age:integer department:references
$ bundle exec rails g model student_subject student:references subject:references
```

親テーブルのモデルにhas_manyで子テーブルのモデルとの関連を追加していきます。

```diff:department.rb
class Department < ApplicationRecord
+  has_many :students
end

```

```diff:subject.rb
class Subject < ApplicationRecord
+  has_many :student_subjects
+  has_many :students, through: :student_subjects
end
```

```diff:student.rb
class Student < ApplicationRecord
  belongs_to :department
+  has_many :student_subjects
+  has_many :subjects, through: :student_subjects
end

```

DBのマイグレーションを行います。

```
$ bundle exec rails db:migrate
```

## seedの作成

サンプルデータを作成します。

```seeds.rb
Department.create(name:'経済学部')
Department.create(name:'外国語学部')
Department.create(name:'社会学部')
Department.create(name:'理工学部')
Subject.create(name:'数学')
Subject.create(name:'社会学')
Subject.create(name:'外国語')
Student.create(name:'鈴木よし子', sex:'女', 'age':18, department_id:1)
Student.create(name:'伊藤佳穂', sex:'女', 'age':19, department_id:2)
Student.create(name:'鈴木佳孝', sex:'男', 'age':20, department_id:3)
Student.create(name:'山田太郎', sex:'男', 'age':22, department_id:4)
StudentSubject.create(student_id:1, subject_id:1)
StudentSubject.create(student_id:1, subject_id:2)
StudentSubject.create(student_id:2, subject_id:3)
StudentSubject.create(student_id:3, subject_id:2)
StudentSubject.create(student_id:4, subject_id:1)
StudentSubject.create(student_id:4, subject_id:3)
```

サンプルデータをDBに格納します。

```
$ bundle exec rails db:seed
```

## コントローラーの作成

コントローラーを作成します。今回は検索フォームのある`index`と検索結果を表示する`search`の二つを定義します。

```
$ bundle exec rails g controller students index search
```

コントローラの実装をしていきます。indexは[ransackのドキュメント](https://github.com/activerecord-hackery/ransack)をもとに作成していきます。

`search_params`はStrong Parametersの実装部分です。リクエストパラメータは後で作成するため一旦`!`(全ての属性のリクエストを許可する)としています。

```students_controller.rb
class StudentsController < ApplicationController
  def index
    @q = Student.ransack(params[:q])
    @students = @q.result(distinct: true)
  end

  def search
    @q = Student.search(search_params)
    @students = @q.result(distinct: true)
  end

  private
  def search_params
    params.require(:q).permit!
  end
end
```

## ルーティングの設定

ルーティングの設定をします。

```routes.rb
Rails.application.routes.draw do
  root to: 'students#index'
  get 'search', to: 'students#search'
end
```

## トップページの作成

まずはサンプルデータの一覧の表示させてみます。
今回はSlimを利用してviewを作成していきます。ERBの代わりにSlimを利用するための手順は[こちら](http://qiita.com/nishina555/items/8ed180333fbc8f78e91e)を参考にしてみてください。
検索フォームは`search_form`というviewにrenderをしています。
renderをするのはわかりやすくするためですので、index.html.slimに直接書いても構いません。

```view/students/index.html.slim
h1
  | 学生検索
= render 'search_form'
table
  - @students.each do |student|
    tr
      td
        = student.name
      td
        = student.sex
      td
        = student.age
      td
        = student.department.name
      td
        = student.subjects.map{|subject_id| Subject.find(subject_id).name}.join(', ')
```

検索フォームを実装するviewを作成します。`url:search_path`でsearchというパスに検索フォームから入力されたクエリを送っています。
`ransack`というgemを利用することで`search_form_for`というヘルパーメソッドが利用できるようになります。

```view/students/_search_form.html.slim
= search_form_for(@q, url:search_path) do |f|
/検索フォームをここに書く
= f.submit
```

検索結果を表示するviewを作成します。

```view/students/search.html.slim
h1
  | 検索結果
table
  - @students.each do |student|
    tr
      td
        = student.name
      td
        = student.sex
      td
        = student.age
      td
        = student.department.name
      td
        = student.subjects.map{|subject_id| Subject.find(subject_id).name}.join(', ')
= link_to 'トップ', root_path
```

## 起動確認

ここまでで動作確認をしてみます。

```
$ bundle exec rails s
```
以下のような画面が表示されていればOKです。(この時点でsearchボタンを押しても検索クエリが空なのでエラーになります。)

![mock.png](https://qiita-image-store.s3.amazonaws.com/0/140792/0903a3ff-9e28-9d8d-8a14-906977857c6e.png)



## N+1問題の解決

上記の設定でトップページを表示することができますが、このプログラムにはN+1問題が存在しています。
`bullet`というgemを利用するとN+1問題を検知することができるので確認をしてみます。
N+1問題や`bullet`については[こちら](http://www.techscore.com/blog/2012/12/25/rails%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E7%B4%B9%E4%BB%8B-n1%E5%95%8F%E9%A1%8C%E3%82%92%E6%A4%9C%E5%87%BA%E3%81%99%E3%82%8B%E3%80%8Cbullet%E3%80%8D/)などを参考にしてみてください。

`bullet`の[公式ドキュメント](https://github.com/flyerhzm/bullet)を参考に以下のようにGemfileとdevelopment.rbを変更します。

```Gemfile
group :development, :test do
  gem 'bullet'
end
```

```
$ bundle install
```

```development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
  Bullet.add_footer = true
end
```

ここまでで動作確認をしてみます。

```
$ bundle exec rails s
```

http://localhost:3000/ にアクセスするとポップアップが表示されます。これはN+1問題が存在していることを意味しています。

![bullet_error.png](https://qiita-image-store.s3.amazonaws.com/0/140792/dc3819f7-c342-8fa4-3e41-c39719cdad10.png)


エラー内容に従い以下のようにcontrollerを変更します。

```diff:students_controller.rb
class StudentsController < ApplicationController
  def index
    @q = Student.ransack(params[:q])
-   @students = @q.result(distinct: true)
+   @students = @q.result.includes(:department, :subjects)
  end

  def search
    @q = Student.search(search_params)
-   @students = @q.result(distinct: true)
+   @students = @q.result.includes(:department, :subjects)
  end
```
これで改めて起動をすると次はポップアップが表示されないと思います。これでN+1問題が解決されたことになります。

# 検索フォームの作成

ここから検索フォームの作成をしていきます。

前提知識として、`ransack`について軽く説明をします。
`ransack`では検索する属性にpredicate(述語)を繋ぐことで条件検索をすることができます。
predicateについては[Ransackのススメ](http://qiita.com/nysalor/items/9a95d91f2b97a08b96b0)や[公式のドキュメント](https://github.com/activerecord-hackery/ransack)を参考にしてみてください。

例えば、`name`という属性に対して、入力した文字列を含む(contain)という条件検索をしたい場合、predicateを属性に繋げて`name_cont`とすることで検索をすることができます。実装は以下のようになります。

## テキストフィールドを用いた検索

例として、学生の氏名(name)にある文字列が含まれている(cont)という条件で検索をするフォームを作成します。

```_search_form.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :name_cont, '名前'
    = f.search_field :name_cont
  = f.submit
```

事前準備で仮で作成をしたStrong Parametersを変更します。
ここでは、検索パラメーターとして`:name_cont`のみを許可させるようにStrong Parametersを変更します。

```diff:students_controller.rb
def search_params
-    params.require(:q).permit!
+    params.require(:q).permit(:name_cont)
end
```

これで以下のような検索フォームができたと思います。

![search_name.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/5c56438f-34d6-cdaf-1f56-2ad5541292f2.gif)

## セレクトボックスを用いた検索(オブジェクトから選択肢を作成)

例として、学部のセレクトボックスを作成していきます。
Railsでは[collection_select](http://railsdoc.com/references/collection_select)を利用するとデータベースの情報を元に選択肢を生成できます。(= オブジェクトを利用して選択肢を作成)
今回のサンプルはdepartmentsというテーブルがあるので、collection_selectを利用してセレクトボックスを作成していきます。
`:include_blank`をオプションとして設定することで、選択しない(条件指定しない)という実装ができます。

```diff:students_controller.rb
def index
  @q = Student.ransack(params[:q])
+ @departments = Department.all
  @students = @q.result.includes(:department, :subjects)
end
```

```_search_form.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :department_id_eq, '学部'
    = f.collection_select :department_id_eq, @departments, :id, :name, :include_blank => '指定なし'
  = f.submit
```

```students_controller.rb
def search_params
  params.require(:q).permit(:department_id_eq)
end
```

これで以下のような検索フォームができたと思います。

![search_department.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/08278fc3-0cb3-8631-d274-13a451b3b329.gif)


## ラジオボタンを用いた検索(オブジェクトから選択肢を作成)

上記の例と少し似ていますが次に、例として、学部のラジオボタンを作成していきます。
ここでは`collection_radio_buttons`(collection_selectのラジオボタン版)を利用します。


```_search.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :department_id_eq, '学部'
    = f.collection_radio_buttons :department_id_eq, @departments, :id, :name
  = f.submit
```

![search_department_radio_defo.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/334943e1-03ed-769d-a53e-40405126504c.gif)


ただ、このままではラジオボタンは一回押すと取り消しができないので検索条件から外すことができなくなります。
「指定しない」のようなラジオボタンを追加するためには以下のようにします。`:checked => true`はデフォルトでチェックを入れている状態にするオプションです。

```diff:_search.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :department_id_eq, '学部'
+    = f.radio_button 'department_id_eq', '', {:checked => true}
+    | 指定なし
    = f.collection_radio_buttons :department_id_eq, @departments, :id, :name
  = f.submit
```

これで以下のような検索フォームができたと思います。

![search_department_radio.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/1afc75f4-07a9-a412-efcf-46933db6c2f0.gif)


## ラジオボタンを用いた検索(オブジェクトを利用しない場合)

例として、性別のラジオボタンを作成していきます。
上記のラジオボタンはオブジェクトから選択肢を生成していましたが、今回はオブジェクトとして特に定義されていない属性に関するラジオボタンの作成方法です。`_eq`は「等しい」という条件検索を行うpredicateです。
Strong Parametersも`params.require(:q).permit(:sex_eq)`のように変更するのを忘れないでください。

```search.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :sex_eq, '性別'
    = f.radio_button :sex_eq, '', {:checked => true}
    | 指定なし
    = f.radio_button :sex_eq, '男'
    | 男
    = f.radio_button :sex_eq, '女'
    | 女
= f.submit
```

これで以下のような検索フォームができたと思います。

![search_sex.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/6341c8a5-9b98-75ab-b1a1-2a4e81fdfe8f.gif)


「〜より上」のような検索も同様のやりかたで作成できます。
例として年齢のラジオボタンを作成していきます。

```search.html.slim
= search_form_for(@q, url:search_path) do |f|
  p
    = f.label :age_gteq, '年齢'
    = f.radio_button 'age_gteq', '',{:checked => true}
    | 指定なし
    = f.radio_button 'age_gteq', '15'
    | 15歳以上
    = f.radio_button 'age_gteq', '20'
    | 20歳以上
  = f.submit
```

これで以下のような検索フォームができたと思います。

![search_age.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/956071fb-fad8-55ff-cbb7-dd3bfae5b5ac.gif)

## チェックボックスを利用した検索

例として、履修科目のチェックボックスを作成していきます。
履修科目はsubjectsというテーブルが存在しているので`collection_check_boxes`でフォームを作成します。
`_in`で「どれかにあてはまる」という条件検索ができます。Strong Parametersの変更も忘れないようにしてください。


```diff:students_controller.rb
def index
  @q = Student.ransack(params[:q])
+ @subjects = Subjects.all
  @students = @q.result.includes(:department, :subjects)
end
```

```
= search_form_for(@q, url:search_path) do |f|
  p
    =  f.label :subjects_id_in, '履修科目'
    =  f.collection_check_boxes :subjects_id_in, @subjects, :id, :name
  = f.submit
```

これで以下のような検索フォームができたと思います。

![search_subject.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/dfdca653-8b2a-4ace-a103-508a44ef0704.gif)



# 検索クエリの受け渡し方法

ここからは検索条件で利用したクエリを表示する方法を説明します。
Searchボタンを押した際、ログには以下のようなものが表示されているので、これを利用することで画面に検索クエリを表示することができます。

```
Parameters: {"utf8"=>"✓", "q"=>{"name_cont"=>"鈴木", "department_id_eq"=>"1", "sex_eq"=>"女", "age_gteq"=>"15", "subjects_id_in"=>["", "2"]}, "commit"=>"Search"}
```

例えば以下のように作成するとそれぞれの検索フォームに値が入力された場合、クエリを表示するようになります。
なお履修科目(`subjects_id_in`)の部分の`reject(&:blank?).empty?`の意味ですが、上記のログを見てわかるようにチェックボックスのパラメーターには`[""]`という空の文字列が配列で渡されています。なので、`reject(&:blank?)`で一旦空文字を取り除き、その後クエリが存在するかのチェックを行なっています。

```_search.html.slim
h1
  | 検索クエリ
- unless params[:q][:name_cont].empty?
  | 名前:
  = params[:q][:name_cont]
  br
- unless params[:q][:department_id_eq].empty?
  | 学部:
  = Department.find(params[:q][:department_id_eq]).name
  br
- unless params[:q][:sex_eq].empty?
  | 性別:
  = params[:q][:sex_eq]
  br
- unless params[:q][:age_gteq].empty?
  | 年齢:
  = params[:q][:age_gteq]
  | 歳以上
  br
- unless params[:q][:subjects_id_in].reject(&:blank?).empty?
  | 履修科目:
  = params[:q][:subjects_id_in].reject(&:blank?).map{|subject_id| Subject.find(subject_id).name}.join(', ')
```

今までの実装と合わせて最終的には以下のような検索フォームができたと思います。（いちばんはじめに掲載した今回のゴールです。）

![search_complete.gif](https://qiita-image-store.s3.amazonaws.com/0/140792/f2a89697-d711-ad53-a722-48bcc23b8ab9.gif)

# おわりに

以上で検索フォームの作成方法の説明を終わります。今回の内容で検索フォームのほとんどのパターンは網羅できたと思っているのですが、「こういった場合はどうすればいいのか」といったことがありましたらコメントをください。
また、より良い実装方法や記事内に間違いなどがあればご指摘ください。


ツイッター(<a href="https://twitter.com/nishina555" target="_blank" rel="noopener">@nishina555</a>)やってます。フォローしてもらえるとうれしいです！



# 参考

[ransack](https://github.com/activerecord-hackery/ransack)
[RailsでERBの代わりにSlimを利用する手順と、おすすめSlim学習方法](http://qiita.com/nishina555/items/8ed180333fbc8f78e91e)
[Railsライブラリ紹介: N+1問題を検出する「bullet」](http://www.techscore.com/blog/2012/12/25/rails%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E7%B4%B9%E4%BB%8B-n1%E5%95%8F%E9%A1%8C%E3%82%92%E6%A4%9C%E5%87%BA%E3%81%99%E3%82%8B%E3%80%8Cbullet%E3%80%8D/)
[検索条件フォームのようにテーブルと完全に同一でないフォームもform_forを使って実装できる](http://qiita.com/ishidamakot/items/dc16b6e22e6ec275079f)
[collection_check_boxesでhiddenタグを挿入されないようにする方法](http://qiita.com/munky69rock/items/febd65ba35dc183052f0)