はじめに
アプリケーションを作るうえで検索フォームを実装する機会はとても多いと思います。
今回は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で子テーブルのモデルとの関連を追加していきます。
class Department < ApplicationRecord
+ has_many :students
end
class Subject < ApplicationRecord
+ has_many :student_subjects
+ has_many :students, through: :student_subjects
end
class Student < ApplicationRecord
belongs_to :department
+ has_many :student_subjects
+ has_many :subjects, through: :student_subjects
end
DBのマイグレーションを行います。
$ bundle exec rails db:migrate
seedの作成
サンプルデータを作成します。
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の実装部分です。リクエストパラメータは後で作成するため一旦!
(全ての属性のリクエストを許可する)としています。
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
ルーティングの設定
ルーティングの設定をします。
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に直接書いても構いません。
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
というヘルパーメソッドが利用できるようになります。
= search_form_for(@q, url:search_path) do |f|
/検索フォームをここに書く
= f.submit
検索結果を表示するviewを作成します。
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
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を変更します。
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_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を変更します。
def search_params
- params.require(:q).permit!
+ params.require(:q).permit(:name_cont)
end
これで以下のような検索フォームができたと思います。
セレクトボックスを用いた検索(オブジェクトから選択肢を作成)
例として、学部のセレクトボックスを作成していきます。
Railsではcollection_selectを利用するとデータベースの情報を元に選択肢を生成できます。(= オブジェクトを利用して選択肢を作成)
今回のサンプルはdepartmentsというテーブルがあるので、collection_selectを利用してセレクトボックスを作成していきます。
:include_blank
をオプションとして設定することで、選択しない(条件指定しない)という実装ができます。
def index
@q = Student.ransack(params[:q])
+ @departments = Department.all
@students = @q.result.includes(:department, :subjects)
end
= 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
def search_params
params.require(:q).permit(:department_id_eq)
end
これで以下のような検索フォームができたと思います。
ラジオボタンを用いた検索(オブジェクトから選択肢を作成)
上記の例と少し似ていますが次に、例として、学部のラジオボタンを作成していきます。
ここではcollection_radio_buttons
(collection_selectのラジオボタン版)を利用します。
= 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_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_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_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の変更も忘れないようにしてください。
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?)
で一旦空文字を取り除き、その後クエリが存在するかのチェックを行なっています。
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タグを挿入されないようにする方法