Ruby
Rails
ransack

ransackerを使ってRansackの検索を拡張する

More than 3 years have passed since last update.

Ruby on Railsで検索機能を作る際に便利なRansackというgemがあります。複数の条件で検索する画面を作るときにとても役に立つライブラリなのですが、特殊な検索条件を指定しようとすると標準の機能では対応できないことがあります。

ransackerを使って検索機能を拡張することで、解決できる場合があります。


Ransackで指定できない条件の例


カラム同士の比較

注文テーブルに「納品予定日」と「納品日」というカラムがあって、納品日が納品予定日より後の日付になっているもの(納期遅れ)を抽出したい。


イコールや範囲指定で表せない条件

年と月は指定せず、日の部分が1日のものを抽出したい。とか、金額が1,000の倍数になっているものを抽出したい。


ransackerでできること

ransackerは、検索の対象となる項目を定義することができるメソッドです。SQL文のWHERER句に記述する条件の左側に相当する部分。Ransackの条件文でいうと、述語の前に来る部分を独自に定義することができます。


SQL文のWHERE句

 NAME        LIKE    'hoge'

^^^^
[検索の対象] [演算子] [指定する値]


Ransackの条件

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を呼びます。


app/models/order.rb

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に渡ります。


app/views/order/index.html.erb

    <%= f.label :appointed_day_of_month_eq, '納品予定日(年月の指定なし)' %>

<%= f.select :appointed_day_of_month_eq, [*1..31], include_blank: true %>

上の例では述語としてeqを使っていますが、ltgtで以上や以下の条件でも同じように使えます。


他の例


注文日が月の最終日


app/models/orders.rb

    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直書きせずできました。


app/models/orders.rb

    ransacker :delivery_delayed, type: :integer do |parent|

Arel::Nodes::InfixOperation.new('<',
parent.table[:appointed_date], parent.table[:delivery_date])
end