Ransackのススメ

  • 420
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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`"

動的に組み立てる際には便利ですが、実装せずにテストが通ってしまったりするのでテストコードにマッチ・アンマッチ双方のテストを書くなど工夫しましょう。