Ruby on Railsで検索機能を作る際に便利なRansackというgemがあります。複数の条件で検索する画面を作るときにとても役に立つライブラリなのですが、特殊な検索条件を指定しようとすると標準の機能では対応できないことがあります。
ransacker
を使って検索機能を拡張することで、解決できる場合があります。
Ransackで指定できない条件の例
カラム同士の比較
注文テーブルに「納品予定日」と「納品日」というカラムがあって、納品日が納品予定日より後の日付になっているもの(納期遅れ)を抽出したい。
イコールや範囲指定で表せない条件
年と月は指定せず、日の部分が1日のものを抽出したい。とか、金額が1,000の倍数になっているものを抽出したい。
ransackerでできること
ransacker
は、検索の対象となる項目を定義することができるメソッドです。SQL文のWHERER句に記述する条件の左側に相当する部分。Ransackの条件文でいうと、述語の前に来る部分を独自に定義することができます。
NAME LIKE 'hoge'
^^^^
[検索の対象] [演算子] [指定する値]
NAME_LIKE: 'hoge'
NAME:[検索の対象]
LIKE:[述語]
`hoge':[指定する値]
上の「NAME」の部分には、多くの場合テーブルのカラム名が指定されます。これをカスタマイズして、独自の条件を定義するのがransacker
メソッドです。
これができると何が嬉しいかというと、これまで「納品日 = ○○月○○日」としか書けなかった条件が、「納品日と納品予定日の差 > 0日」とか、「納品予定日の曜日 = 土日」みたいな条件が書けるようになります。
そして他の項目と一緒にsearch_form_for
の中に検索条件の入力項目として並べることができます。
ransackerの使い方
ransacker
メソッドはモデルクラスに記述します。privateのところに書くのがいいようです。
パラメータ
ransacker name, (option) {|parent| block}
name
検索対象につける名前です。ここでつけた名前がransackの条件文字列で使えるようになります。
オプション
キー | 内容 |
---|---|
formatter | 指定する値をフォーマットするブロック。省略すると値はそのまま渡されます。 |
type | 検索対象の型を指定します。指定した値はこの型と一致するよう変換されます。デフォルトは文字列型です。 |
args | わかりません |
callable | 後述するブロックをオプションでも指定できます |
ブロック
検索対象を表すArelのNodeオブジェクトを返すブロックです。
パラメータにはparent
というものが渡されてきます。詳しいことはよくわかりませんが、こいつのtable
メソッドから、定義されたモデルのArel::Table
オブジェクトが手に入ります。なので、parent.table[:attr_name]
を返すようにしておくと、指定した属性を表すNodeオブジェクトが検索対象になります。
このブロックの戻り値をカスタマイズして独自の検索対象を定義するわけですが、SQLを直接書いて指定することもできるので、Arelに詳しくない私でもなんとかなりました。
実装例
モデルクラスのprivate部分でransacker
を呼びます。
class Order < ActiveRecord::Base
...
private
# 納品予定日の日部分(1〜31)を条件にする
ransacker :appointed_day_of_month,
formatter: -> v { format('%02d', v)} do |parent|
Arel.sql("strftime('%d', appointed_date)")
end
end
appointed_day_of_month
という名前で、納品日から日の部分を取得するSQL(WHERE句の左側になるところ)を定義しています。
[26] pry(main)> Order.ransack(appointed_day_of_month_eq: 9).result
Order Load (0.4ms) SELECT "orders".* FROM "orders" WHERE strftime('%d', appointed_date) = '09'
実行すると上のようなSQLが発行されます。
formatter
で0埋めの2桁に変換するProcを指定しているので、指定した9
という値は'09'
に変換されてSQLに渡ります。
<%= f.label :appointed_day_of_month_eq, '納品予定日(年月の指定なし)' %>
<%= f.select :appointed_day_of_month_eq, [*1..31], include_blank: true %> 日
上の例では述語としてeq
を使っていますが、lt
やgt
で以上や以下の条件でも同じように使えます。
他の例
注文日が月の最終日
ransacker :appointed_date_is_last_day, type: :integer do |parent|
Arel.sql(<<-EOSQL.gsub(/\n/,''))
(date("orders"."appointed_date", 'start of month','+1 month','-1 day')
== "orders"."appointed_date")
EOSQL
end
納品日が納品予定日より後
Arelを使用した例。単純な変換ならこのArel::Nodes::InfixOperation
というものを使ってSQL直書きせずできました。
ransacker :delivery_delayed, type: :integer do |parent|
Arel::Nodes::InfixOperation.new('<',
parent.table[:appointed_date], parent.table[:delivery_date])
end