Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

記事の構成は以下のようになっています。
まず、サンプルとなる学生検索アプリケーションの作成方法を説明します。次に検索フォームの作成方法の説明をします。最後に検索クエリを表示する方法を説明します。
検索フォームの作成のみを知りたい場合は「検索フォームの作成」からお読みください。

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

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

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

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

アプリの完成図

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

search_complete.gif

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

サンプルデータについて

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

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

ER図

サンプルデータを作るためのER図は以下のようにします。
今回のER図作成にはBurntSushi/erdを利用しました。

student_search-erd.png

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

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

具体的なレコード

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

スクリーンショット 2017-03-03 1.25.24.png

事前準備

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

アプリ作成

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

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

mock.png

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

bullet_error.png

エラー内容に従い以下のように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

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

search_name.gif

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

例として、学部のセレクトボックスを作成していきます。
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

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

search_department.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

ただ、このままではラジオボタンは一回押すと取り消しができないので検索条件から外すことができなくなります。
「指定しない」のようなラジオボタンを追加するためには以下のようにします。: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

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

search_department_radio.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

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

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

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

例として、履修科目のチェックボックスを作成していきます。
履修科目は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_subject.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

おわりに

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

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

参考

ransack
RailsでERBの代わりにSlimを利用する手順と、おすすめSlim学習方法
Railsライブラリ紹介: N+1問題を検出する「bullet」
検索条件フォームのようにテーブルと完全に同一でないフォームもform_forを使って実装できる
collection_check_boxesでhiddenタグを挿入されないようにする方法

nishina555
Webデベロッパーです。現在は業務委託で仕事をしています。サーバーサイドがメイン。Rails/React/Redux/Node/GraphQL/AWS。大学院時代は自然言語処理の研究を行っていました。
https://nishinatoshiharu.com/
onecareer
ワンランク上のキャリアを目指す学生のための新卒採用サービスONE CAREERの開発・運営会社
https://www.onecareer.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした