Edited at

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


はじめに

アプリケーションを作るうえで検索フォームを実装する機会はとても多いと思います。

今回はRails(バージョンは5.0.1)を利用した色々な検索フォームの作成方法をまとめます。前提としてransackというgemを利用しています。

記事の構成は以下のようになっています。

まず、サンプルとなる学生検索アプリケーションの作成方法を説明します。次に検索フォームの作成方法の説明をします。最後に検索クエリを表示する方法を説明します。

検索フォームの作成のみを知りたい場合は「検索フォームの作成」からお読みください。

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


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

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


  • 1:多、多:多関係のモデルの作成および表示方法

  • N+1問題の解決方法

  • 色々な検索フォームの作成方法

  • 検索クエリの表示方法


アプリの完成図

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

なお、今回のソースコードはこちらに置いておきました。


サンプルデータについて

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

氏名
性別
年齢
学部
履修科目

鈴木よし子

18
経済学部
数学・社会学

伊藤佳穂

19
外国語学部
外国語

鈴木佳孝

20
社会学部
社会学

山田太郎

22
理工学部
数学・外国語


ER図

サンプルデータを作るためのER図は以下のようにします。

今回のER図作成にはBurntSushi/erdを利用しました。

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


  • 学生:学部は多:1の関係

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


具体的なレコード

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


事前準備

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


アプリ作成

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

$ rails new student_search

$ cd student_search

今回利用するransackを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で子テーブルのモデルとの関連を追加していきます。


department.rb

class Department < ApplicationRecord

+ has_many :students
end



subject.rb

class Subject < ApplicationRecord

+ has_many :student_subjects
+ has_many :students, through: :student_subjects
end


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のドキュメントをもとに作成していきます。

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を利用するための手順はこちらを参考にしてみてください。

検索フォームは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ボタンを押しても検索クエリが空なのでエラーになります。)


N+1問題の解決

上記の設定でトップページを表示することができますが、このプログラムにはN+1問題が存在しています。

bulletというgemを利用するとN+1問題を検知することができるので確認をしてみます。

N+1問題やbulletについてはこちらなどを参考にしてみてください。

bullet公式ドキュメントを参考に以下のようにGemfileとdevelopment.rbを変更します。

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問題が存在していることを意味しています。

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


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のススメ公式のドキュメントを参考にしてみてください。

例えば、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を変更します。


students_controller.rb

def search_params

- params.require(:q).permit!
+ params.require(:q).permit(:name_cont)
end

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


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

例として、学部のセレクトボックスを作成していきます。

Railsではcollection_selectを利用するとデータベースの情報を元に選択肢を生成できます。(= オブジェクトを利用して選択肢を作成)

今回のサンプルはdepartmentsというテーブルがあるので、collection_selectを利用してセレクトボックスを作成していきます。

:include_blankをオプションとして設定することで、選択しない(条件指定しない)という実装ができます。


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

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


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

上記の例と少し似ていますが次に、例として、学部のラジオボタンを作成していきます。

ここでは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

ただ、このままではラジオボタンは一回押すと取り消しができないので検索条件から外すことができなくなります。

「指定しない」のようなラジオボタンを追加するためには以下のようにします。:checked => trueはデフォルトでチェックを入れている状態にするオプションです。


_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

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


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

例として、性別のラジオボタンを作成していきます。

上記のラジオボタンはオブジェクトから選択肢を生成していましたが、今回はオブジェクトとして特に定義されていない属性に関するラジオボタンの作成方法です。_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.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

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


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

例として、履修科目のチェックボックスを作成していきます。

履修科目はsubjectsというテーブルが存在しているのでcollection_check_boxesでフォームを作成します。

_inで「どれかにあてはまる」という条件検索ができます。Strong Parametersの変更も忘れないようにしてください。


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ボタンを押した際、ログには以下のようなものが表示されているので、これを利用することで画面に検索クエリを表示することができます。

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(', ')

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


おわりに

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

また、より良い実装方法や記事内に間違いなどがあればご指摘ください。

ツイッター(@nishina555)やってます。フォローしてもらえるとうれしいです!


参考

ransack

RailsでERBの代わりにSlimを利用する手順と、おすすめSlim学習方法

Railsライブラリ紹介: N+1問題を検出する「bullet」

検索条件フォームのようにテーブルと完全に同一でないフォームもform_forを使って実装できる

collection_check_boxesでhiddenタグを挿入されないようにする方法