Railsのアプリに検索フォームを付けたいけどどうやるの?
簡単にgemで実装できないかな
検索フォームはWebサービスで定番の機能ですが、いざ実装しようとするとやり方が分からなくて悩んでしまいますよね。
いろいろと情報を集めてみると、gemを使ったり、使わないで自力で実装したりと解説している人によって結構差があり「どれを使えばいいのかますますわからなくなった……」なんてこともしばしば起こります。
そこで、この記事ではRailsアプリに簡単に検索フォームを実装する方法を紹介します。基本からgemを使った方法、応用的な検索方法まで解説していきますので、この記事を読めば検索フォームを使いこなせるようになるでしょう。
Railsで検索フォームを実装するためのパターン
はじめに、Railsで検索フォームを実装するパターンを2つ紹介します。
- Ransack
- gemを使わずに実装する
ザックリいえば「gemを使って機能を外部から取り込む」か「システムの中に検索フォームを作り込む」か。この2択になります。
今回は検索フォームのgemとして一番知名度が高く、よく利用されていると思われるRansackを中心に見ていきましょう。
Ransackを使うメリットはなんと言っても導入が簡単なことと、導入の簡単さに比べて検索方法が豊富なことです。導入自体はいつものgemなので、Gemfileに書き込んでbundleすればOK。簡単ですね。
具体的な使い方に関しては次の章で見ていくことにします。
逆に、Ransackを使うデメリットは、以下のものが挙げられます。
- gem同士の衝突
- 利用箇所がわからなくなる可能性
- gemが更新停止する危険性
以下でRansackを利用するデメリットの詳細を見ていきましょう。
gem同士の衝突
gem同士の衝突はたくさんのgemを導入していると起こりうる問題です。コンフリクトともいい、gem同士の依存関係などから問題が発生します。
解決策としては後ろ向きですが、やたらめったらgemを導入しない、ということです。
利用箇所がわからなくなる可能性
利用箇所がわからなくなる可能性は、長期に渡ってサービスを運用していると起こりうる問題点です。対象のgem利用箇所がシステムのかなり深い部分に入り込んでしまい、更新しようにもどこをいじったらいいのかわからなくなってしまった、ということが実際におきています。
解決策として、システムの根っこになるような部分はそもそもgemに依存させないのが重要です。
gemが更新停止する危険性
gemが更新停止する危険性は読んで字のごとく、です。
gemは個人開発のものも多く、10年後にも開発が続いているかと言われると微妙なものがたくさん存在します。組織であっても継続的なサポートを保証されているものはほとんど無く、OSSによるバックアップがあるとはいえ、不人気なものはいつかは更新が止まってしまうでしょう。
そうした時に、システムも連動して破綻してしまう可能性はgemを使う以上常に潜んでいる、ということです。
解決策としては、gemをなるべく使わない。使うとしてもgemの内容を自分で書けるくらいに習熟しておく、といったところでしょう。
いろいろとgem利用に関する問題点を列挙しましたが、これらは要するにビジネスサービスとして、5年10年使うことを考えた時に致命的になってくる問題点です。正味1年も運営できるのか見込みが立たないサービスであれば、気軽にgemを突っ込んでしまって問題ないでしょう。
次章ではgemのRansackを使った検索フォームの実装方法を紹介します。
Ransackを使った簡単検索フォームの実装
それでは、Ransackをつかって検索フォームを実装していきましょう。サンプルとして、scaffold(スキャフォールド)をつかったアプリを立ち上げていきます。
サンプルアプリとRansackの導入
サンプルアプリの名前は ransack_sample として、userテーブルに name(string),age(integer) をもつとしましょう。
以下のコマンドを実行することで、サンプルアプリの雛形が出来上がります。
rails new ransack_sample cd ransack_sample/ rails generate scaffold user name:string age:integer rails db:migrate
つづいて、GemfileにRansackを追加し、bundleでインストールします。
Gemfile gem 'ransack'
ターミナル
bundle
つづいて、users_controller.rb と index.html.erb を以下のように書き換えます。
検索フォームを追加し、コントローラーに検索の動作を追加しました。
users_controller.rb のindexアクションを以下のように書き換えます。
def index @q = User.ransack(params[:q]) @users = @q.result(distinct: true) end
index.html.erbの <h1>Users</h1> と <table> の間に以下のフォームを挿入します。
<%= search_form_for @q do |f| %> <%= f.label :name_cont %> <%= f.search_field :name_cont %> <%= f.submit %> <% end %>
これで検索フォームを実装したscaffoldのサンプルアプリが完成しました。サーバを立ち上げて、実際の画面を確認してみてください。
以下のような画面が表示されればOKです。検索窓とDBの内容を表示する部分ができていますね。
検索機能の確認
検索機能を試すために適当にデータを入れてみましょう。今回はテストデータとして下記の3つを登録してみます。
- 山田 タカオ 54歳
- 伊藤 タカオ 23歳
- 山田 ヨシコ 36歳
検索窓から山田を入力してみると「山田 タカオ」と「山田ヨシコ」がヒットします。
同様にタカオで検索すると「山田 タカオ」と「伊藤 タカオ」がヒットしますね。
問題なく検索フォームが機能しているのが確認できたでしょうか。
ここまでのまとめ動画
Ransackの導入から実際に使うまでを動画にまとめました。細かい操作を確認しながら進めたい方は以下の動画をご覧ください。
検索機能の基本・ビューについて
実際に動くモデルができたところで、Ransackの基本的な使い方を見てみましょう。
Ransackには検索フォームを作るモードが2種類用意されています。それが、シンプルモードとアドバンスモードです。
アドバンスモードは入れ子になったAND/OR検索など、かなり複雑なことが実現できます。ですが、今回は基本的な利用法の紹介が主眼ですので、以下すべてシンプルモードで解説をしていきます。
アドバンスモードなどのさらなる情報が欲しい方は公式をご覧ください。
⇨ Ransack
まずはビューから見ていきましょう。普通のフォームを作る際は form_for や form_with を使いますが、Ransackで検索フォームを作る場合、search_form_forメソッドを利用します。
search_form_forの引数に今回は@qを取りました。@qのクエリ情報を元に検索を行います。
続いてフォームの中身に移ります。フォームの中身は以下のような記述でした。
<%= f.label :name_cont %> <%= f.search_field :name_cont %> <%= f.submit %>
f.label、 f.search_field、 f.submit自体は見慣れている方も多いことでしょう。それぞれラベル、検索フィールド、送信ボタンを形成するフォームですね。
Ransackを用いた検索で特徴的なのが、これらの後ろについている「:name_cont」の部分です。これはnameカラムに対してLike句を使った部分一致検索を行う、という意味になります。文字にすると難しそうに見えますが、要するに前の節でやった山田さんやタカオを抽出したアレです。
Ransackはnameで対応するカラムを、_contで検索方法を柔軟に切り替えることができます。ここがRansackを利用するメリットといえますね。
_contの部分をRansackでは述語(predicate)といいます。せっかくですので、よく使いそうな述語をいくつか見ていきましょう。
eq | 完全に一致したものを抽出します。 |
lt |
ある値より小さいものを抽出します。 |
gt |
ある値より大きなものを抽出します。 |
in |
与えられた配列に含まれるものを抽出します。 |
cont,not_cont |
文字列が含まれるものを抽出します。not_をつけることで、文字列が含まれないものを抽出に切り替える事ができます。 |
start,end |
startは与えられた文字列が先頭と一致するものを抽出します。いわゆる前方一致検索です。 endは逆に後方一致検索を行います。 |
他にも様々な述語がありますので、興味のある方は調べてみましょう。
検索機能の基本・コントローラーについて
今度はコントローラーについて見ていきましょう。今回はusers_controller.rbのindexアクションを以下のように書き換えたのでした。
def index @q = User.ransack(params[:q]) @users = @q.result(distinct: true) end
中身を見てみると、params[:q] はおなじみの検索パラメータから q を取得する操作です。今回は検索フィールド内に書き込まれた検索クエリと、カラムと述語の情報がransackメソッドに渡されて、必要な情報が検索されます。
結果を@usersに入れて表示というのが今回のサンプルアプリの流れだったのでした。
なお、result(distinct: true)についてですが、distinctはSQLにある重複を排除する機能です。trueにしておくことで結果から重複した内容のものを排除できます。
Ransackを使った様々な検索
最後にRansackを使って様々な検索をしてみましょう。
年齢の範囲検索
この節では年齢の範囲を指定した検索の方法を見ていきましょう。特定の日付や値段、IDの範囲検索といった応用が考えられますね。
ビューを改造していきましょう。今回はageカラムを検索範囲として、下限の年齢と上限の年齢をセットするフィールドが必要になりますね。そのため、index.html.erbのsearch_form_for以下を次のように書き換えます。
<%= search_form_for @q do |f| %> <%= f.label :age_gt %> <%= f.search_field :age_gt %> <%= f.label :age_lt %> <%= f.search_field :age_lt %> <%= f.submit %> <% end %>
これで年齢の範囲検索ができるようになりました。簡単ですね!
検索範囲の限定(セキュリティ対策)
つづいて、検索範囲の限定を学んでおきましょう。現在の検索フィールドは意図的にパラメータを加工することで、こちらが指定した以外のカラムの検索もできてしまいます。
意図しないカラムの検索を防止するためにも、カラムの検索範囲を制限することがセキュリティ的に望まれます。その方法を見ていきましょう。
カラムの検索範囲の制限はモデルから行うことができます。今回はUserモデルをつかっているので、app/models/user.rbを編集していきます。
編集結果は以下の通りです。
class User < ApplicationRecord #追加部分 def self.ransackable_attributes(auth_object = nil) %w[name age] end def self.ransackable_associations(auth_object = nil) [] end #追加部分終わり end
それぞれ役割を見ていきましょう。
ransackable_attributesは検索対象に許可するカラムを指定する部分です。デフォルトではすべてのカラムが対象になっていますが、今回利用するname,ageカラムに制限をかけています。ここはアプリによって変わってくるので、適宜変更が必要になりますね。
ransackable_associationsは検索対象に含める関連についての設定です。ここは空の配列でオーバーライドすることで、必要のない関連を検索することを防いでいます。
Ransackで検索フォームを設定する場合は、これらの制限をかけてセキュリティも担保するようにしましょう。
まとめ
いかがでしたか?
今回はRansackを使った簡単な検索フォームの実装と応用的な使い方とセキュリティで気をつけたい部分を紹介してきました。
簡単な検索フォームであれば自分で実装するのもありですが、ここまで簡単に使えるなら、問題が発生しない範囲で積極的に使っていきたいですね。
ぜひ有用なgemを活用して、生産性の高い開発を行っていきましょう!