ransackの検索機能

今回はgem ransackで検索機能を実装します。
以前の記事でフリーワード検索を実装したのですが、今回は作成日での検索とセレクトボックスでの検索を実装していきます。
1つ前の記事で作ったscaffoldアプリを元にやっていきます。
このアプリではi18nenumenum_helpを導入済みです。
ではいってみましょう。

ransackをインストール

gemfileに

gem 'ransack'

として

bundle install

します。

コントローラー編集

コントローラーで検索結果を受け取れるようにします。

  def index
    # @users = User.all これを削除
    @q = User.ransack(params[:q]) #この2行を
    @users = @q.result(distinct: true) #追加します
  end

ransackメソッド送られてきたパラメーターを元にテーブルからデータを検索するメソッド
(params[:q])検索パラメータを取得
resultメソッドransackメソッドで取得したデータをオブジェクトに変換するメソッド
(distinct: true)検索結果の重複を取り除く

viewの編集

ついでにviewをちょっと見やすく編集します。

users/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Gender</th>
      <th>Created_at</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= render @users %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>
users/_user.html.erb

<tr>
  <td><%= user.name %></td>
  <td><%= user.gender_i18n %></td>
  <td><%= user.created_at %></td>
  <td><%= link_to 'Show', user %></td>
  <td><%= link_to 'Edit', edit_user_path(user) %></td>
  <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>

ユーザー情報部分を部分テンプレートとして切り出して、作成日時も表示されるようにしました。

Image from Gyazo
こんな感じです。

検索フォームの作成

セレクトボックスで検索

まずはセレクトボックスで、性別で検索できるようにします。

users/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]} %>
  <%= f.submit %>
<% end %>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Gender</th>
      <th>Created_at</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= render @users %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

search_form_forransackが提供するメソッド
f.selectのところがややこしいので1つ1つ見ていきます。
まず第一引数の:gender_eqですが、選択した性別と等しいものを探すという意味です。eqはイコール(equal)の略ですね。
そしてややこしい第二引数。
User.genders_i18n.invertまでは前回の記事を参照してください。

irb(main):001:0> User.genders_i18n.invert
=> {"男性"=>"male", "女性"=>"female", "秘密"=>"secret"}
irb(main):002:0> User.genders_i18n.invert.map{|key,value|[key,User.genders[value]]}
=> [["男性", 0], ["女性", 1], ["秘密", 2]]

この2つを比べてみます。
違いは

  • ハッシュから配列に変わっている
  • male, female, secretから0, 1, 2に変わっている

の2点、違いがあります。
ハッシュから配列に変わっているのはmapメソッドによるものです。
mapメソッドは、配列orハッシュ.map {|変数名| 実行する処理 }で戻り値を配列で返します。
今回のややこしいところに当てはめてみると、

irb(main):001:0> User.genders_i18n.invert
=> {"男性"=>"male", "女性"=>"female", "秘密"=>"secret"}

これを配列の形になおして欲しい。
配列の中身は|key, value|という形にしてくれ。
keyはそのままkey("男性"=>"male"の"男性")、
valueはUser.genders[value]つまり

irb(main):003:0> User.genders
=> {"male"=>0, "female"=>1, "secret"=>2}

valueの部分("male"=>0の0)
つまり最終的な形が

irb(main):002:0> User.genders_i18n.invert.map{|key,value|[key,User.genders[value]]}
=> [["男性", 0], ["女性", 1], ["秘密", 2]]

になる、という流れです。
これでセレクトボックスによる検索が実装できました。
Image from Gyazo

最後に指定なしでも検索できるように

<%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>

include_blankを設定しておきます。
複数条件で検索する時に必要です。

作成日で検索

次は作成日での検索です。
例えば5/1の00:00から5/10の23:59で検索したいとします。

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>
  <%= f.date_field :created_at_gteq %>
  <span></span>
  <%= f.date_field :created_at_lteq %>
  <%= f.submit %>
<% end %>

date_field日付の入力欄を生成
gteqgreater than equalの略
lteqless than equalの略
created_at_gteqで指定した日付以降
created_at_lteqで指定した日付まで

Image from Gyazo

こんな感じで日付で検索できます。
しかし実際に検索してみると

Image from Gyazo

5/10の15:00:00に作成したyamadaさんが表示されていません。
created_at_lteqだと、その日の00:00までの範囲で検索してしまうからです。
これを23:59までにするにはpredicateのカスタマイズが必要です。

predicateのカスタマイズ

まずconfig/initializers/ransack.rbを作成します。

Ransack.configure do |config|
  config.add_predicate 'lteq_end_of_day', # 名前を付ける
                       arel_predicate: 'lteq', # 'lteq'をカスタマイズしますよ
                       formatter: proc { |v| v.end_of_day } # end_of_dayメソッドを実行
end

これでカスタマイズできたので検索フォームを修正してみます。

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>
  <%= f.date_field :created_at_gteq %>
  <span></span>
  <%= f.date_field :created_at_lteq_end_of_day %>
  <%= f.submit %>
<% end %>

これで

Image from Gyazo

想定通りの検索ができました。

最後に

自分的には難しい内容でした。コンソールを使ったりして流れを追って、なんとかやってることはわかった、ぐらいの感じです。
このロジックを自分で考えるのはちょっと今の理解度では厳しいな、と思いました。アウトプットを続けて知識を定着させていきたいです。
あと参考にしたサイトとかいつも載せてなかったんですが載せないとダメですよね、忘れてました。すいません。
今日は疲れたので次回から載せます…
最後まで読んでいただきありがとうございました。間違いなどありましたらコメントいただけますと幸いです。