Railsアプリで検索機能を実装するケースは非常に多いと思います。
簡単な検索であればwhereとLIKEを使って書けますし、やや複雑なものもeverywhereが便利ですが、ここではもっと複雑な条件の組み合わせを実装する時に便利なransackを紹介します。
基本
searchメソッドで条件を指定し、resultメソッドで結果を返します。
Item.search(:name_cont => 'ほげ').result
resutはActiveRecord::Relationを返すので、SQLは普通のActiveRecord同様遅延評価されますし、さらにwhereを繋げたり、kaminariでページングしたりすることもできます。また、to_sqlで発行されるSQLを確認することもできます。
もう少し詳しく書くと、searchはModelまたはActiveRecord::RelationをレシーバにしてRansack:Searchを返し、Ransack:search#resultはActiveRecord::Relationを返します。
searchの書き方
searchメソッドは一種のDSLになっています。
属性をpredicate(述語)で繋いだ文字列のシンボルをキーに、検索対象を値にして書いていきます。
前述の例だと「name属性に'ほげ'という文字列を含む(countain)」対象を検索します。以下のようなSQLが発行されます。
Item.search(:name_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` LIKE '%ほげ%')"
以下のようなpredicateがあります。
eq
等しいものにマッチします。
Item.search(:name_eq => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` = 'ほげ')"
単体だとwhereで十分なので意味がありませんが、複数の条件を動的に組み合わせる際に便利です。
not_eqで「等しくないもの」になります。
lt
ある値より小さいものにマッチします。
Item.search(:price_lt => 1000).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`price` < 1000)"
lteqだと「以下」になります。
gt
ある値より大きいものにマッチします。
Item.search(:price_gt => 1000).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`price` > 1000)"
gteqだと「以上」になります。
in
SQLのinです。含まれるものにマッチします。
Item.search(:category_id_in => [5,10,15,20]).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`category_id` IN (5,10,15,20))"
not_inで「含まれないもの」になります。
cont
前述したように「文字列が含まれるもの」にマッチします。
not_contで「文字列が含まれないもの」になります。
start
「特定の文字列が先頭のもの」にマッチします。
Item.search(:name_start => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` LIKE 'ほげ%')"
endで「特定の文字列が末尾のもの」になります。
predicateはこの他にもあるので、wikiを参考にするといいでしょう。
組み合わせ
複数の属性を組み合わせることができます。
その場合、andかorで繋ぎます。
Item.search(:name_and_description_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') AND (`items`.`description` LIKE '%ほげ%')))"
Item.search(:name_or_description_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') OR (`items`.`description` LIKE '%ほげ%')))"
複数条件
条件を組み合わせることができます。
末尾に_allを付けると全ての条件にマッチ、_anyだといずれかにマッチするものを検索します。
Item.search(:name_cont_all => ['ほげ', 'ひげ']).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') AND (`items`.`name` LIKE '%ひげ%')))"
Item.search(:name_cont_any => ['ほげ', 'ひげ']).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') OR (`items`.`name` LIKE '%ほげ%')))"
関連モデルの検索
関連先を条件に含めることができます。
Item.search(:comments_body_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` LEFT OUTER JOIN `item_comments` ON `item_comments`.`item_id` = `items`.`id` WHERE `item_comments`.`body` LIKE '%ほげ%'"
注意点
存在しないカラムを指定すると例外を返さず無視されます。
Item.search(:unknownattribute_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items`"
動的に組み立てる際には便利ですが、実装せずにテストが通ってしまったりするのでテストコードにマッチ・アンマッチ双方のテストを書くなど工夫しましょう。