猫Rails

ねこー🐈

Ransackで簡単に検索フォームを作る73のレシピ

はじめに

登場人物の紹介

👦🏻 ぼく太くん。新米プラグラマー。

🐱 猫先輩。プログラミング歴1年の頼りになる先輩。猫だからかわいい。

プロローグ

👦🏻 むーん…

🐱 どうしたんだい、ぼく太くん?

👦🏻 あ、猫先輩!Rails難しいですよー。検索フォームが作れないですよー。

🐱 ふむふむ…なーるほどね。これだったら、Ransackを使えばちょちょいのちょいで検索フォームを作れるよ。

👦🏻 らんさっく?

🐱 Ransackって言うのはね…

環境

🐱 Rubyやgemのバージョンは、執筆時点での安定バージョンを対象としているよ。

Ruby:2.4
Rails:5.1
Ransack:1.8
MySQL: 5.7

第1章 Ransackをはじめよう

001 Ransackとは?

👦🏻 Ransackってなにー?

🐱 Ransackを使うと検索フォームを簡単に作れるようになるんだよ。検索結果のソートも簡単にできるし、ちょっと手間をかければ複雑な条件を組み合わせた検索フォームだって作ることができるんだよ。

👦🏻 ほえーん。

🐱 百聞は一見にしかずだ。早速使ってみよう。

002 セットアップ

🐱 まずはGemfileにransackを追加してね。

gem 'ransack'

🐱 次にbundle installコマンドを実行すると、RailsアプリケーションでRansackが使えるようになるよ。

$ bundle install

🐱 お疲れ様。セットアップはこれで終わりだよ。

003 使ってみよう

🐱 Product(商品)の検索フォームを作っていくよ。name(商品名)カラムを持ったProduct(商品)があると想定してね。

コントローラー

🐱 検索画面にはindexアクションを使うよ。params[:q]には検索パラメータが渡されるから、それを@search = Product.ransack(params[:q])としてあげれば、@searchという検索オブジェクトが作成されるよ。さらにこの@searchに対して@products = @search.resultとしてあげれば検索結果が得られるんだよ。

class ProductsController < ApplicationController
  def index
    # 検索オブジェクト
    @search = Product.ransack(params[:q])
    # 検索結果
    @products = @search.result
  end
end

🐱 コントローラーはだいたいいつもこんな感じになるよ。Ransackではビュー側でparams[:q]をいかに作るかがポイントになるよ。それじゃあポイントになるビューに行ってみよう。

ビュー

🐱 search_form_forform_forのRansack版だよ。search_form_forの引数に検索オブジェクト@searchをとることで検索フォームを作れるんだ。そして一番のポイントはf.search_field :name_contだよ。これでnameに対してLIKE句を利用した部分一致検索ができるようになるんだ。こんな感じのSQLになるよ。

SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%入力した値%')

🐱 この*_contの部分を変えるだけで柔軟にSQLを組み立てられるのが、Ransackの強みなんだよ。

# `form_for`のRansack版
<%= search_form_for @search do |f| %>
  # nameカラムに対して部分一致検索ができる
  <%= f.label :name_cont, "商品名を含む" %>
  <%= f.search_field :name_cont %>

  # 検索ボタン
  <div class="actions"><%= f.submit "検索" %></div>
<% end %>

🐱 お疲れ様。これがRansackの基本的な使い方になるよ。Ransackには複雑なSQLを組み立てる方法がたくさん用意されているけれど、どれもこの流れがベースになるから覚えておいてね。

👦🏻 ほいーん。

🐱 (伝わっているのか…?)

第2章 シンプルモードで検索する

004 シンプルモードとは?

🐱 Ransackには検索フォームを作る方法が2つあるんだ。シンプルモード(Simple Mode)とアドバンストモード(Advanced Mode)だよ。今回はシンプルモードについて説明していくよ。

👦🏻 シンプルモードだから、どうせシンプルなんでしょ?

🐱 その通りだよ。実はさっきの003 使ってみようで作った検索フォームはシンプルモードの検索フォームなんだ。カラム名と述語(Predicate)を組み合わせてSQLを組み立てていくんだよ。

005 述語とは?

👦🏻 述語(Predicate)って何?

🐱 f.search_field :name_contcontのことだよ。name_contとするとLIKE句を使った曖昧検索ができるし、name_eqとすれば=を使った検索ができるようになるんだよ。

006 eq - =検索

🐱 SQLはこんな感じになるよ。

Product.ransack(name_eq: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`name` = 'ほげ'"

🐱 resultで検索結果を作って、to_sqlでSQLの文字列に変換してるよ。

🐱 ビューはこんな感じだよ。

f.text_field :name_eq

007 matches - LIKE検索

🐱 SQLはこんな感じになるよ。

Product.ransack(name_matches: "ほ%げ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE 'ほ%げ')"

🐱 ビューはこんな感じだよ。

f.text_field :name_matches

008 cont - LIKE検索(部分一致)

🐱 SQLはこんな感じになるよ。

Product.ransack(name_cont: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ほげ%')"

🐱 ビューはこんな感じだよ。

f.text_field :name_cont

🐱 ちなみにcontはcontain(含む)のことだよ。

009 start - LIKE検索(前方一致)

🐱 SQLはこんな感じになるよ。

Product.ransack(name_start: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE 'ほげ%')"

🐱 ビューはこんな感じだよ。

f.text_field :name_start

010 end - LIKE検索(後方一致)

🐱 SQLはこんな感じになるよ。

Product.ransack(name_end: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ほげ')"

🐱 ビューはこんな感じだよ。

f.text_field :name_end

011 gt - >検索

🐱 SQLはこんな感じになるよ。

Product.ransack(price_gt: "100").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`price` > 100)"

🐱 ビューはこんな感じだよ。

f.text_field :price_gt

🐱 ちなみにgtはgreater than(より大きい)の略だよ。

012 gteq - >=検索

🐱 SQLはこんな感じになるよ。

Product.ransack(price_gteq: "100").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`price` >= 100)"

🐱 ビューはこんな感じだよ。

f.text_field :price_gteq

🐱 ちなみにgteqはgreater than or equal to(以上)の略だよ。

013 lt - <検索

🐱 SQLはこんな感じになるよ。

Product.ransack(price_lt: "100").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`price` < 100)"

🐱 ビューはこんな感じだよ。

f.text_field :price_lt

🐱 ちなみにltはless than(より小さい)の略だよ。

014 lteq - <=検索

🐱 SQLはこんな感じになるよ。

Product.ransack(price_lteq: "100").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`price` <= 100)"

🐱 ビューはこんな感じだよ。

f.text_field :price_lteq

🐱 ちなみにlteqはless than or equal to(以下)の略だよ。

015 true - trueの検索

🐱 SQLはこんな感じになるよ。

Product.ransack(awesome_true: true).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`awesome` = 1"

🐱 trueの他にも'true', 'TRUE', 't', 'T', 1, '1'などが真になるよ。

Product.ransack(awesome_true: 'true').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`awesome` = 1"

🐱 逆にfalseの場合はこうなるよ。

Product.ransack(awesome_true: false).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`awesome` != 1)"

🐱 falseの他にも'false', 'FALSE', 'f', 'F', 0, '0'などが偽になるよ。

Product.ransack(awesome_true: 'false').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`awesome` != 1)"

016 false - falseの検索

🐱 SQLはこんな感じになるよ。

Product.ransack(awesome_false: true).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`awesome` = 0"

🐱 trueの他にも'true', 'TRUE', 't', 'T', 1, '1'などが真になるよ。

Product.ransack(awesome_false: 'true').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`awesome` = 0"

🐱 逆にfalseの場合はこうなるよ。

Product.ransack(awesome_false: false).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`awesome` != 0)"

🐱 falseの他にも'false', 'FALSE', 'f', 'F', 0, '0'などが偽になるよ。

Product.ransack(awesome_false: 'false').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`awesome` != 0)"

🐱 述語falseと述語trueは反対の関係にあるよ。でもNULL値を含むかどうかの微妙な違いがあるから、NULL値が存在する場合には注意して使ってね。

017 blank - blank?の検索

🐱 NULLか空文字だったらマッチするよ。blank?メソッドに近い感じだね。SQLはこんな感じになるよ。

Product.ransack(name_blank: true).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` IS NULL OR `products`.`name` = '')"

🐱 逆にfalseの場合はこうなるよ。

Product.ransack(name_blank: false).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` IS NOT NULL AND `products`.`name` != '')"

018 present - present?の検索

🐱 NULLでも空文字でもなければマッチするよ。present?メソッドに近い感じだね。SQLはこんな感じになるよ。

Product.ransack(name_present: true).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` IS NOT NULL AND `products`.`name` != '')"

🐱 逆にfalseの場合はこうなるよ。

Product.ransack(name_present: false).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` IS NULL OR `products`.`name` = '')"

🐱 述語blankとは逆の動きをするんだね。

019 null - NULLの検索

🐱 SQLはこんな感じになるよ。

Product.ransack(name_null: true).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`name` IS NULL"

020 in - IN検索

🐱 ArrayとRangeが使えるよ。

🐱 Arrayの場合、SQLはこんな感じになるよ。

Product.ransack(price_in: [200, 210]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`price` IN (200, 210)"

🐱 Rangeの場合、SQLはこんな感じになるよ。

Product.ransack(price_in: 200..210).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`price` IN (200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210)"

021 not - 否定

🐱 述語の前にnotをつけることで、否定が可能になるよ。name_eqに対して、name_not_eqのようにしてあげれば、!=による検索ができるんだ。

Product.ransack(name_not_eq: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` != 'ほげ')"

🐱 name_not_contであれば、NOT LIKE検索が可能だよ。

Product.ransack(name_not_cont: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` NOT LIKE '%ほげ%')"

🐱 他にも*_not_null, *_not_in, *_does_not_match_any, *_not_eq_all, *_not_start, *_not_endなどが利用できるよ。

022 all - ANDで繋ぐ(値が複数)

🐱 末尾にallをつけることで、複数の値に対してANDで連結して検索できるよ。name_eqに対して、name_eq_allのようにしてあげれば、複数の値で=による検索ができるよ。

Product.ransack(name_eq_all: ["ほげ", "ぴよ"]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` = 'ほげ' AND `products`.`name` = 'ぴよ')"

🐱 name_cont_allであれば、LIKE検索が可能だよ。

Product.ransack(name_cont_all: ["ほげ", "ぴよ"]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ほげ%' AND `products`.`name` LIKE '%ぴよ%')"

🐱 他にもlt_all, lteq_all, gt_all, gteq_all, matches_all, start_all, end_allなどが利用できるよ。

023 any - ORで繋ぐ(値が複数)

🐱 末尾にanyをつけることで、複数の値に対してORで連結して検索できるよ。name_eqに対して、name_eq_anyのようにしてあげれば、複数の値で=による検索が可能になるよ。

Product.ransack(name_eq_any: ["ほげ", "ぴよ"]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` = 'ほげ' OR `products`.`name` = 'ぴよ')"

🐱 name_cont_anyであれば、LIKE検索が可能だよ。

Product.ransack(name_cont_any: ["ほげ", "ぴよ"]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ほげ%' OR `products`.`name` LIKE '%ぴよ%')"

🐱 他にもlt_any, lteq_any, gt_any, gteq_any, matches_any, start_any, end_anyなどが利用できるよ。

🐱 allOR版だね。

024 and - ANDで繋ぐ(カラムが複数)

🐱 andを使うと、複数のカラムをANDで一気に検索できるよ。first_namelast_nameを同時に検索したい場合はfirst_name_and_last_name_eqとなるよ。

User.ransack(first_name_and_last_name_eq: "ほげ").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = 'ほげ' AND `users`.`last_name` = 'ほげ')"

🐱 first_name_and_last_name_contであれば、LIKE検索が可能だよ。

User.ransack(first_name_and_last_name_cont: "ほげ").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` LIKE '%ほげ%' AND `users`.`last_name` LIKE '%ほげ%')"

025 or - ORで繋ぐ(カラムが複数)

🐱 orを使うと、複数のカラムをORで一気に検索できるよ。first_namelast_nameを同時に検索したい場合はfirst_name_or_last_name_eqとなるよ。

User.ransack(first_name_or_last_name_eq: "ほげ").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = 'ほげ' OR `users`.`last_name` = 'ほげ')"

🐱 first_name_or_last_name_contであれば、LIKE検索が可能だよ。

User.ransack(first_name_or_last_name_cont: "ほげ").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` LIKE '%ほげ%' OR `users`.`last_name` LIKE '%ほげ%')"

026 関連

🐱 Ransackでは関連先を条件に含めるのも簡単だよー。ここではpost has_many comments(PostとCommentが1対多)だと想定してね。post_title_eqみたいに関連先_関連先のカラム_述語とすることで、関連先を条件に含めて検索できるよ。

Comment.ransack(post_title_eq: "ほげ").result.to_sql
=> "SELECT `comments`.* FROM `comments` LEFT OUTER JOIN `posts` ON `posts`.`id` = `comments`.`post_id` WHERE `posts`.`title` = 'ほげ'"

🐱 関連先がhas_manyの場合はcommentsのように複数形にする必要があるから注意してね。

Post.ransack(comments_body_eq: "ほげ").result.to_sql
=> "SELECT `posts`.* FROM `posts` LEFT OUTER JOIN `comments` ON `comments`.`post_id` = `posts`.`id` WHERE `comments`.`body` = 'ほげ'"

🐱 関連が多段になっている場合は、post_user_name_eqみたいに関連先1_関連先2_関連先2のカラム_述語とすればOKだよ。

Comment.ransack(post_user_name_eq: "ほげ").result.to_sql
=> "SELECT `comments`.* FROM `comments` LEFT OUTER JOIN `posts` ON `posts`.`id` = `comments`.`post_id` LEFT OUTER JOIN `users` ON `users`.`id` = `posts`.`user_id` WHERE `users`.`name` = 'ほげ'"

🐱 簡単だね(๑╹ω╹๑ )

027 条件を組み合わせる

🐱 条件は複数組み合わせて使うことができるよ。商品名に"ほげ"を含む100円以上の商品を検索する場合はこんな感じだよー。

Product.ransack(name_cont: "ほげ", price_gteq: 100).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ほげ%' AND `products`.`price` >= 100)"

🐱 ビューはこんな感じだよ。

<%= search_form_for @search do |f| %>

  # 商品名だよ。
  <%= f.label :name_cont %>
  <%= f.search_field :name_cont %>

  # 価格だよ。
  <%= f.label :price_gteq %>
  <%= f.search_field :price_gteq %>

  <%= f.submit %>
<% end %>

028 範囲検索

🐱 gteqとlteqを一緒に使うことで範囲検索が可能だよ。価格が100円~200円の商品の検索はこんな感じー。

Product.ransack(price_gteq: 100, price_lteq: 200).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`price` >= 100 AND `products`.`price` <= 200)"

🐱 ビューはこんなんー。

<%= search_form_for @search do |f| %>

  # 〜以上の検索
  <%= f.label :price_gteq, "価格" %>
  <%= f.number_field :price_gteq %>

  # 〜以下の検索
  <%= f.label :price_lteq, " ~ " %>
  <%= f.number_field :price_lteq %>

  <%= f.submit %>
<% end %>

029 カラムが存在しない場合

🐱 存在しないカラムを指定すると、その条件は無視されるよ。

Product.ransack(aaaa_eq: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products`"

🐱 存在しない述語を指定しても、その条件は無視されるよ。

Product.ransack(name_aaaa: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products`"

🐱 configのignore_unknown_conditionsをfalseにすることで、条件を無視するんじゃなくて例外を投げるように変更できるよ。

# config/initializers/ransack.rb

Ransack.configure do |config|
  # 条件指定が適切でない場合は、例外を投げる
  config.ignore_unknown_conditions = false
end

030 真っぽい値・偽っぽい値

🐱 true, false, null, blank, presentの5つの述語は、真っぽい値か偽っぽい値かで動作が変わるよ。

🐱 真っぽい値は以下の7つだよ。

  • true
  • ‘true’
  • ‘TRUE’
  • ’t'
  • ’T'
  • ‘1’
  • 1
Product.ransack(awesome_true: true  ).result.to_sql
Product.ransack(awesome_true: 'true').result.to_sql
Product.ransack(awesome_true: 'TRUE').result.to_sql
Product.ransack(awesome_true: 't'   ).result.to_sql
Product.ransack(awesome_true: 'T'   ).result.to_sql
Product.ransack(awesome_true: '1'   ).result.to_sql
Product.ransack(awesome_true: 1     ).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`awesome` = 1"

🐱 偽っぽい値は以下の7つだよ。

  • false
  • ‘false’
  • ‘FALSE’
  • ‘f’
  • ‘F’
  • ‘0’
  • 0
Product.ransack(awesome_true: false  ).result.to_sql
Product.ransack(awesome_true: 'false').result.to_sql
Product.ransack(awesome_true: 'FALSE').result.to_sql
Product.ransack(awesome_true: 'f'    ).result.to_sql
Product.ransack(awesome_true: 'F'    ).result.to_sql
Product.ransack(awesome_true: '0'    ).result.to_sql
Product.ransack(awesome_true: 0      ).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`awesome` != 1)"

第3章 アドバンストモードで検索する

031 アドバンストモードとは?

🐱 シンプルモード(Simple Mode)だとProduct.ransack(name_eq: 入力値)のような形になるでしょ?この場合はWHERE句はname = 入力値のようになるから、ユーザーが指定できるのは右辺の部分だけなんだよね。アドバンストモード(Advanced Mode)を使うと、入力値1 入力値2 入力値3のように、左辺も述語もユーザーが指定できるようになるよ。

🐱 シンプルモードとアドバンストモードを比べるとこうなるよ。

# シンプルモード
q = {name_eq: "ほげ"}
Product.ransack(q).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`name` = 'ほげ'"

# アドバンストモード
q = {
  # conditions(条件)
  "c" => {
    "0" => {
      # attributes(属性)
      "a" => { "0" => { "name" => "name" } },
      # predicate(述語)
      "p" => "eq",
      # values(値)
      "v" => { "0" => { "value" => "ほげ" } }
    }
  }
}
Product.ransack(q).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`name` = 'ほげ'"

🐱 ビューはこんな感じだよ。

# シンプルモード
<%= f.search_field :name_eq %>

# アドバンストモード
# conditions(条件)
<%= f.condition_fields do |c| %>

  # attributes(属性)
  <%= c.attribute_fields do |a| %>
    <%= a.attribute_select %>
  <% end %>

  # predicate(述語)
  <%= c.predicate_select %>

  # values(値)
  <%= c.value_fields do |v| %>
    <%= v.search_field :value %>
  <% end %>

<% end %>

👦🏻 さっぱりわかんない…

🐱 今は細かいところはわからなくても大丈夫。シンプルモードよりも細かく指定できるっていうことが伝われば十分だよ。次ページ以降で1つずつ解説していくね。

032 searchオブジェクトの中身はどーなってるの?

🐱 アドバンストモードのレシピを紹介をする前に、search = User.ransack(q) で作る search について解説するね。searchについて知っておくと、アドバンストモードの理解が捗るから、ぜひ押さえておいてね。

7つの要素

🐱 searchは以下の7つの要素から構成されているんだ。

クラス アクセッサ エイリアス 内容 SQL
Sort Search#sorts s ORDER BY name ASC
Grouping Grouping#groupings g 条件のグループ name = "太郎" OR name = "花子"
Combinator Grouping#combinator m 論理演算子 OR
Condition Grouping#conditions c 条件 name = "太郎"
Attribute Condition#attributes a 属性 name
Value Condition#values v "太郎"
Predicate Condition#predicate p 述語 =

🐱 7つはこんな関係になってるよ。

search
┣sorts
┗base(rootのgrouping)
 ┣groupings(入れ子にできる)  
 ┣combinator
 ┗conditions
   ┣attributes
   ┣values
   ┗predicate
  • searchはsorts・base(rootのgrouping)から成る。
  • groupingはgroupings(入れ子にできる)・conditions・combinatorから成る。
  • conditionはattributes・values・predicateから成る。

searchをRailsコンソールで覗く

🐱 $ rails consoleを使ってsearchの中身を見ていくよ。Railsコンソールでsearchを評価すると、searchの中身が可視化されるよ。

Product.ransack(name_eq: "ほげ")
=> Ransack::Search<class: Product, base: Grouping <conditions: [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>], combinator: and>>

🐱 ここから以下の内容が読み取れるよ。

  • searchRansack::Searchクラスのインスタンスである
  • Productに対する処理である
  • base属性に1つのgroupingを持っている
  • groupingは1つのconditionとcombinatorから成る
  • conditionは<attributes: ["name"], predicate: eq, values: ["ほげ"]>である
  • combinatorはandである

🐱 baseと言うのはrootとなるgroupingだよ。今回は、groupingはrootに1つあるだけだけど、groupingを入れ子にすることで複雑なSQLを構築することができるよ。

Product.ransack(name_eq: "ほげ").base
=> Grouping <conditions: [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>], combinator: and>

🐱 そしてbaseはconditions(conditionの配列)を持っているよ。今回は条件が1つだからconditionは1つだけど、条件が複数ある場合はconditionも複数になるよ。

# 条件が1つ
Product.ransack(name_eq: "ほげ").base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>]

# 条件が2つ
Product.ransack(name_eq: "ほげ", id_eq: 1).base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>, Condition <attributes: ["id"], predicate: eq, values: [1]>]

🐱 そしてconditionを構成するattributes・predicate・valuesはこんな感じになるよ。

Product.ransack(name_eq: "ほげ").base.conditions.first.attributes
=> [Attribute <name>]

Product.ransack(name_eq: "ほげ").base.conditions.first.predicate_name
=> "eq"

Product.ransack(name_eq: "ほげ").base.conditions.first.values
=> [Value <ほげ>]

🐱 そしてcombinatorはandだよ。今回はcombinatorには何も指定していないから、デフォルトのandが採用されているよ。

Product.ransack(name_eq: "ほげ").base.combinator
=> "and"

🐱 sortsは何も指定していないから空配列だよ。

Product.ransack(name_eq: "ほげ").sorts
=> []

ちなみに

🐱 ちなみにbaseではなく、searchに対してconditionsなどを呼び出すことが可能だよ。内部でbaseにメソッドを移譲してくれてるよ。

# Product.ransack(name_eq: "ほげ").base.conditions と同じ
Product.ransack(name_eq: "ほげ").conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>]

🐱 これらの7つの要素には、cなどの1文字のエイリアスが割り当てられているよ。

Product.ransack(name_eq: "ほげ").c
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>]

033 シンプルモードとアドバンストモードの関係

🐱 以下の2つは同じクエリを作るよ。

# シンプルモード
q = { first_name_eq: "太郎" }
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`first_name` = '太郎'"

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "太郎" } }
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`first_name` = '太郎'"

🐱 実はシンプルモードとアドバンストモードはransackメソッドの引数の取り方が違うだけなんだ。どちらの方法でやるにしても、内部的にはValue(値)オブジェクトやPredicate(述語)オブジェクトなどに変換するよ。ransackメソッドの引数の取り方が違うだけで、やってることは同じなんだね。

内部的にはシンプルモードとアドバンストモードに違いはない

🐱 シンプルモードの{first_name_eq: "太郎"}は、アドバンストモードのcondition1つに対応する感じだね。だからシンプルモードでもm(combinator)を使うことで、ORで条件をつなげることができるよ。

q = {first_name_eq: "太郎", last_name_eq: "花子", m: "or"}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' OR `users`.`last_name` = '花子')"

🐱 シンプルモードとアドバンストモードを混ぜることも可能だよ。

q = {
  "g" => {
    # groupingその1(アドバンストモード)
    "0" => {
      "c" => {
        "0" => {
          "a" => { "0" => { "name" => "first_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "太郎" } }
        }
      }
    },
    # groupingその2(シンプルモード)
    "1" => { "last_name_eq" => "山田" }
  },
  "m" => "or"
}

User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' OR `users`.`last_name` = '山田')"

🐱 ransackメソッドがどちらの形でも受け取れるというだけで、Ransackの内部的には違いはないんだね。ただ、アドバンストモードだとビュー側で細かく条件を組み立てられるよ。

034 c(conditions) - 条件

🐱 condition(条件)attributes(属性), values(値), predicate(述語)から成るよ。

# シンプルモード
Product.ransack(name_eq: "ほげ").base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>]

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "ほげ" } },
    }
  }
}
Product.ransack(q).base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>]

🐱 条件が2つある場合は、conditionも2つになるよ

# シンプルモード
Product.ransack(name_eq: "ほげ", id_eq: 1).base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>, Condition <attributes: ["id"], predicate: eq, values: [1]>]

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "ほげ" } },
    },
    "1" => {
      "a" => { "0" => { "name" => "id" } },
      "p" => "eq",
      "v" => { "0" => { "value" => 1 } },
    }
  }
}
Product.ransack(q).base.conditions
=> [Condition <attributes: ["name"], predicate: eq, values: ["ほげ"]>, Condition <attributes: ["id"], predicate: eq, values: [1]>]

作ってみよう

🐱 それじゃあ実際にアドバンストモードで検索画面を作っていくよ。こんな感じで、属性のセレクトボックス・述語のセレクトボックス・値のサーチフィールドを用意して、ユーザーにそれぞれ入力してもらえるようにするよ。

f:id:nekorails:20170530000504p:plain

🐱 ビューはこんな感じだよ。

# index.html.erb

<%= search_form_for @search do |f| %>

  # conditions
  # conditionsやattributesのようにコレクションになるものに対しては、`f.condition_fields`などの`f.*_fields`を利用してね。
  # params[:q][:c]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
  <%= f.condition_fields do |c| %>

    # attributes
    # params[:q][:c]["0"][:a]に対応するよ。
    <%= c.attribute_fields do |a| %>
      # 属性のセレクトボックスだよ。
      # `id`などの全ての属性が選択可能だよ。
      # params[:q][:c]["0"][:a]["0"][:name]に対応するよ。
      <%= a.attribute_select %>
    <% end %>

    # predicate
    # 述語のセレクトボックスだよ。
    # `eq`などの全ての述語が選択可能だよ。
    # params[:q][:c]["0"][:p]に対応するよ。
    <%= c.predicate_select %>

    # values
    # params[:q][:c]["0"][:v]に対応するよ。
    <%= c.value_fields do |v| %>
      # 値のサーチフィールドだよ。
      # params[:q][:c]["0"][:v]["0"]["value"]に対応するよ。
      <%= v.search_field :value %>
    <% end %>

  <% end %>

  <%= f.submit %>
<% end %>

# @products(検索結果)に対する処理
# ...略...

🐱 コントローラーはこんな感じだよ。

# products_controller.rb

def index
  @search = Product.ransack(params[:q])
  # 初期状態の@searchはconditionsが空配列なので、1つだけ初期状態のconditionを作っておくよ。
  # これをやっておかないと、`f.condition_fields`で処理すべきconditionが1つもなくて、画面に何も表示されないよ。
  # 検索した場合はparams[:q]からconditionを作成するから、`@search.conditions`が`empty?`の場合だけ作るようにしてね。
  @search.build_condition if @search.conditions.empty?
  @products = @search.result
end

🐱 これで完成だよ。以下の条件で検索すると、params[:q]とSQLはこんな感じになるよ。

f:id:nekorails:20170530000508p:plain

params[:q]
=> {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "id" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "1" } }
    }
  }
}

Product.ransack(params[:q]).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`id` = 1;"

conditionを2つ用意する

🐱 conditionが2つ欲しい場合は、ビューはそのままでconditionを2つ作ってあげればOKだよ。

# products_controller.rb

def index
  @search = Product.ransack(params[:q])
  # conditionを2つ用意するよ。
  # conditionを作るのはconditionがない時だけでいいよ。
  2.times { |i| @search.build_condition unless @search.conditions[i] }
  @products = @search.result
end

🐱 画面にconditionが2セット用意されるよ。

f:id:nekorails:20170530000511p:plain

condition関係のメソッド

grouping.conditions - conditionsを取得する

grouping.conditions
=> [Condition <attributes: ["id"], predicate: eq, values: [1]>]

# alias
grouping.c
=> [Condition <attributes: ["id"], predicate: eq, values: [1]>]

# searchに対しても使用できるよ。
search.conditions
=> [Condition <attributes: ["id"], predicate: eq, values: [1]>]

grouping.build_condition - conditionを作成する

grouping.build_condition
=> Condition <attributes: [nil], values: [nil]>

# searchに対しても使用できるよ。
search.build_condition
=> Condition <attributes: [nil], values: [nil]>

# 空のattributeを2つ用意する。
# 画面にはattributeのセレクトボックスが2つ表示されるよ。
search.build_condition(attributes: 2)
=> Condition <attributes: [nil, nil], values: [nil]>

# 空のvalueを2つ用意する。
# 画面にはvalueのセレクトボックスが2つ表示されるよ。
search.build_condition(values: 2)
=> Condition <attributes: [nil], values: [nil, nil]>

f.condition_fields - アドバンストモードでcに対応するフィールドを用意する

# conditions
# conditionsやattributesのようにコレクションになるものに対しては、`f.condition_fields`などの`f.*_fields`を利用してね。
# params[:q][:c]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
<%= f.condition_fields do |c| %>

  # <%= c.predicate_select %>などを使う

<% end %>

035 a(attributes) - 属性

🐱 attributeが1つの場合はこんな感じだよ。attributesは["first_name"]だよ。

# シンプルモード
User.ransack(first_name_eq: "ほげ")
=> Ransack::Search<class: User, base: Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["ほげ"]>], combinator: and>>

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "ほげ" } },
    }
  }
}
User.ransack(q)
=> Ransack::Search<class: User, base: Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["ほげ"]>], combinator: and>>

🐱 attributeが2つの場合はこんな感じになるよ。attributesは["first_name", "last_name"]だね。first_nameとlast_nameの2つが検索対象になっているね。

# シンプルモード
User.ransack(first_name_and_last_name_eq: "ほげ")
=> Ransack::Search<class: User, base: Grouping <conditions: [Condition <attributes: ["first_name", "last_name"], predicate: eq, combinator: and, values: ["ほげ"]>], combinator: and>>

# アドバンストモード
# attributeが複数ある場合は場合は、m(combinator)が必要になるよ。
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" }, "1" => { "name" => "last_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "ほげ" } },
      "m" => "and"
    }
  }
}
User.ransack(q)
=> Ransack::Search<class: User, base: Grouping <conditions: [Condition <attributes: ["first_name", "last_name"], predicate: eq, combinator: and, values: ["ほげ"]>], combinator: and>>

attributeを固定する

🐱 a.attribute_selectを使えばattributeをセレクトボックスで選択可能になるでしょ?attributeをidなんかに固定したい場合は、a.attribute_selectの代わりにa.hidden_fieldなんかを使ってあげればいいよ。

# index.html.erb

<%= search_form_for @search do |f| %>

  <%= f.condition_fields do |c| %>

    <%= c.attribute_fields do |a| %>
      # id固定
      <%#= a.attribute_select %>
      <%= a.hidden_field :name, value: :id %>
    <% end %>

    <%= c.predicate_select %>

    <%= c.value_fields do |v| %>
      <%= v.search_field :value %>
    <% end %>

  <% end %>

  <%= f.submit %>
<% end %>


# @products(検索結果)に対する処理
# ...略...

attributeのセレクトボックスを2つ用意する

🐱 attributeを2つ選択できるようにするよ。attributeが複数ある時はcombinatorが必要になるので、combinatorのセレクトボックスも用意するね。画面はこんな感じになるよ。

f:id:nekorails:20170530000515p:plain

🐱 ビューはこんな感じだよ。c.combinator_selectでcombinatorのセレクトボックスを用意してあげてね。

# index.html.erb

<%= search_form_for @search do |f| %>

  <%= f.condition_fields do |c| %>

    <%= c.attribute_fields do |a| %>
      <%= a.attribute_select %>
    <% end %>

    <%= c.predicate_select %>

    <%= c.value_fields do |v| %>
      <%= v.search_field :value %>
    <% end %>

    # combinatorのセレクトボックス。
    # any(or)とall(and)の2つを選択できる。
    <%= c.combinator_select %>
  <% end %>

  <%= f.submit %>
<% end %>

# @users(検索結果)に対する処理
# ...略...

🐱 コントローラーはこんな感じだよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q])
  # 空のattributeを2つ用意する。
  # 画面にはattributeのセレクトボックスが2つ表示されるよ。
  @search.build_condition(attributes: 2) if @search.conditions.empty?

  @users = @search.result
end

🐱 これで完成だよ。以下の条件で検索すると、params[:q]とSQLはこんな感じになるよ。

f:id:nekorails:20170530000520p:plain

params[:q]
=> {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" }, "1" => { "name" => "last_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "ほげ" } },
      "m" => "or"
    }
  }
}

User.ransack(params[:q]).resutl.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = 'ほげ' OR `users`.`last_name` = 'ほげ')"

attribute関係のメソッド

condition.attributes - attributesを取得

condition.attributes
=> [Attribute <first_name>]

# alias
condition.a
=> [Attribute <first_name>]

c.attribute_fields - アドバンストモードでaに対応するフィールドを用意する

<%= search_form_for @search do |f| %>

  <%= f.condition_fields do |c| %>

    # attributes
    # params[:q][:c]["0"][:a]に対応するよ。
    <%= c.attribute_fields do |a| %>
      <%= a.attribute_select %>
    <% end %>

    # ...略...

c.attribute_select - アドバンストモードでattributeのセレクトボックスを用意する

# `id`などの全ての属性からなるセレクトボックスだよ。
# params[:q][:c]["0"][:a]["0"][:name]に対応するよ。
# デフォルトでは全ての属性が選択可能だよ。
<%= a.attribute_select %>

# `associations`オプションを使えば、関連先の属性も検索対象にできるよ。
<%= a.attribute_select associations: [:posts] %>

f:id:nekorails:20170530000527p:plain

036 p(predicate) - 述語

predicateを固定する

🐱 c.predicate_selectを使えばpredicateをセレクトボックスで選択可能になるでしょ?predicateをeqなんかに固定したい場合は、c.predicate_selectの代わりにc.hidden_fieldなんかを使ってあげればいいよ。

<%= search_form_for @search do |f| %>

  <%= f.condition_fields do |c| %>

    <%= c.attribute_fields do |a| %>
      <%= a.attribute_select %>
    <% end %>

    # predicateをidに固定
    <%#= c.predicate_select %>
    <%= c.hidden_field :p, value: "eq" %>

    <%= c.value_fields do |v| %>
      <%= v.search_field :value %>
    <% end %>
  <% end %>


  <%= f.submit %>
<% end %>




# @products(検索結果)に対する処理
# ...略...

predicate関係のメソッド

c.predicate_select - アドバンストモードでpredicateのセレクトボックスを用意する

# デフォルトでは、カスタム述語を含む全ての述語を選択できるよ。
<%= c.predicate_select %>

# `only`オプションで選択可能な述語を指定できるよ。
# この場合eqとcontだけ選択可能だよ。
<%= c.predicate_select only: [:eq, :cont] %>

# `compounds`オプションをfalseにすれば、`*_any`と`*_all`の述語を除外できるよ。
<%= c.predicate_select compounds: false %>

037 v(values) - 値

複数ある場合

🐱 valueが1つの場合はこんな感じだよ。

# シンプルモード
User.ransack(first_name_eq: "太郎").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`first_name` = '太郎'"

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "太郎" } },
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`first_name` = '太郎'"

🐱 valueが2つの場合はこんな感じになるよ。predicateには*_any(ORで繋げる)*_all(ANDで繋げる)のどちらかを利用することになるよ。

# シンプルモード
User.ransack(first_name_eq_any: ['太郎', '花子']).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' OR `users`.`first_name` = '花子')"

# アドバンストモード
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" } },
      "p" => "eq_any",
      "v" => {
        "0" => { "value" => "太郎" },
        "1" => { "value" => "花子" }
      }
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' OR `users`.`first_name` = '花子')"

predicate関係のメソッド

c.value_fields - アドバンストモードでvに対応するフィールドを用意する

<%= search_form_for @search do |f| %>

  <%= f.condition_fields do |c| %>

    # values
    # params[:q][:c]["0"][:v]に対応するよ。
    <%= c.value_fields do |v| %>
      # 値を入力してもらうよ。
      # params[:q][:c]["0"][:v]["0"]["value"]に対応するよ。
      <%= v.search_field :value %>
    <% end %>


    # ...略...

038 g(groupings) - 条件グループ

基本的な使い方

🐱 groupingが2つあって、各groupingにconditionが2つずつあるような画面を作っていくよ。

search
┗base
  ┣grouping
  ┃ ┣condition
  ┃ ┗condition
  ┗grouping
    ┣condition
    ┗condition

f:id:nekorails:20170530000531p:plain

🐱 SQLはこんな感じになるよ。

q = {
  "g" => {
    # 条件グループ1
    "0" => {
      "c" => {
        "0" => {
          "a" => { "0" => { "name" => "first_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "太郎" } }
        },
        "1" => {
          "a" => { "0" => { "name" => "first_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "花子" } }
        }
      },
      "m" => "or",
    },
    # 条件グループ2
    "1" => {
      "c" => {
        "0" => {
          "a" => { "0" => { "name" => "last_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "山田" } }
        },
        "1" => {
          "a" => { "0" => { "name" => "last_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "田中" } }
        }
      },
      "m" => "or",
    }
  },
  # 条件グループの論理演算子
  # 省略するとandになるよ。
  "m" => "and"
}

User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE ((`users`.`first_name` = '太郎' OR `users`.`first_name` = '花子') AND (`users`.`last_name` = '山田' OR `users`.`last_name` = '田中'))"

🐱 ビューはこんな感じだよ。f.grouping_fieldsがポイントだよ。

<%= search_form_for @search do |f| %>
  # groupings
  # params[:q][:g]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
  <%= f.grouping_fields do |g| %>
    <%= g.condition_fields do |c| %>

      <%= c.attribute_fields do |a| %>
        <%= a.attribute_select %>
      <% end %>

      <%= c.predicate_select %>

      <%= c.value_fields do |v| %>
        <%= v.search_field :value %>
      <% end %>

    <% end %>

    # 2つのconditionを繋げるAND/ORのセレクトボックス
    <%= g.combinator_select %>

    <br/>
  <% end %>

  # 2つのgroupingを繋げるAND/ORのセレクトボックス
  <%= f.combinator_select %>

  <%= f.submit %>
<% end %>


# @users(検索結果)に対する処理
# ...略...

🐱 コントローラーはこんな感じだよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q])

  # groupingを2つ用意する。
  # 各groupingに対して、conditionを2つずつ用意する。
  # 検索した場合は`User.ransack(params[:q])`でgrouping/conditionが作られるので、その場合はそっちを利用する。
  2.times do |i|
    grouping = @search.groupings[i] || @search.build_grouping
    2.times { |i2| grouping.conditions[i2] || grouping.build_condition }
  end

  @users = @search.result
end

🐱 これで完成だよ。

groupingを入れ子にする

🐱 groupingは入れ子にすることで、複雑なSQLを組み立てることが可能だよ。groupingの中にgroupingが2つずつあるような画面を作っていくよ。

search
┗base
  ┣grouping
  ┃ ┣grouping
  ┃ ┃┗condition
  ┃ ┗grouping
  ┃   ┗condition
  ┗grouping
    ┣grouping
    ┃┗condition
    ┗grouping
      ┗condition

f:id:nekorails:20170530000536p:plain

🐱 SQLはこんな感じだよ。

q = {
  "g" => {
    # 条件グループ1
    "0" => {
      "g" => {
        # 条件グループ1.1
        "0" => {
          "c" => {
            "0" => {
              "a" => { "0" => { "name" => "first_name" } },
              "p" => "eq",
              "v" => { "0" => { "value" => "太郎" } }
            }
          }
        },
        # 条件グループ1.2
        "1" => {
          "c" => {
            "0" => {
              "a" => { "0" => { "name" => "first_name" } },
              "p" => "eq",
              "v" => { "0" => { "value" => "花子" } }
            }
          }
        }
      },
      # 条件グループ1の論理演算子
      "m" => "or"
    },
    # 条件グループ2
    "1" => {
      "g" => {
        # 条件グループ2.1
        "0" => {
          "c" => {
            "0" => {
              "a" => { "0" => { "name" => "last_name" } },
              "p" => "eq",
              "v" => { "0" => { "value" => "山田" } }
            }
          }
        },
        # 条件グループ2.2
        "1" => {
          "c" => {
            "0" => {
              "a" => { "0" => { "name" => "last_name" } },
              "p" => "eq",
              "v" => { "0" => { "value" => "田中" } }
            }
          }
        }
      },
      # 条件グループ2の論理演算子
      "m" => "or"
    }
  },
  # base(rootの条件グループ)の論理演算子
  "m" => "and"
}

User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE ((`users`.`first_name` = '太郎' OR `users`.`first_name` = '花子') AND (`users`.`last_name` = '山田' OR `users`.`last_name` = '田中'))"

# 入れ子になっているよ
User.ransack(q).base.groupings.size
=> 2
User.ransack(q).base.groupings.first.groupings.size
=> 2

🐱 ビューはこんな感じになるよ。f.grouping_fieldsを2回使って入れ子にしているよ。

<%= search_form_for @search do |f| %>
  # grouping_fieldsを2回使ってるよ。
  <%= f.grouping_fields do |g1| %>
    <%= g1.grouping_fields do |g2| %>

      <%= g2.condition_fields do |c| %>

        <%= c.attribute_fields do |a| %>
          <%= a.attribute_select %>
        <% end %>

        <%= c.predicate_select %>

        <%= c.value_fields do |v| %>
          <%= v.search_field :value %>
        <% end %>

      <% end %>
    <% end %>
    <%= g1.combinator_select %>
    <br/>
  <% end %>
  <%= f.combinator_select %>
  <%= f.submit %>
<% end %>

🐱 コントローラーはこんな感じだよ。

def index
  @search = User.ransack(params[:q])

  2.times do |i|
    grouping = @search.groupings[i] || @search.build_grouping
    2.times do |i2|
      grouping2 = grouping.groupings[i2] || grouping.build_grouping
      grouping2.build_condition if grouping2.conditions.empty?
    end
  end

  @users = @search.result
end

groupings関係のメソッド

grouping.groupings - groupingsを取得する

grouping.groupings
=> [Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["太郎"]>]>, Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["花子"]>]>]

# alias
grouping.g
=> [Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["太郎"]>]>, Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["花子"]>]>]

# searchに対しても使用できるよ。
# baseに移譲される
search.groupings
=> [Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["太郎"]>]>, Grouping <conditions: [Condition <attributes: ["first_name"], predicate: eq, values: ["花子"]>]>]

grouping.build_grouping - groupingを作成する

grouping.build_grouping
=> Grouping <>

# searchに対しても使用できるよ。
search.build_grouping
=> Grouping <>

f.grouping_fields - アドバンストモードでgに対応するフィールドを用意する

# groupings
# params[:q][:g]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
<%= f.grouping_fields do |g| %>
  # ...略...
<% end %>

039 m(combinator) - 論理演算子

🐱 combinatorは論理演算子(and/or)だよ。conditionsとgroupingsに対して使うことになるよ

conditionsに使う

# and
User.ransack(first_name_eq: "太郎", last_name_eq: "田中", m: "and").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' AND `users`.`last_name` = '田中')"

# or
User.ransack(first_name_eq: "太郎", last_name_eq: "田中", m: "or").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' OR `users`.`last_name` = '田中')"

# デフォルトはandになるよ。
User.ransack(first_name_eq: "太郎", last_name_eq: "田中").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' AND `users`.`last_name` = '田中')"

# and(アドバンストモード)
q = {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "first_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "太郎" } }
    },
    "1" => {
      "a" => { "0" => { "name" => "last_name" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "田中" } }
    }
  },
  "m" => "and"
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' AND `users`.`last_name` = '田中')"

groupingsに使う

🐱 groupingsにも使えるよ。使い方はconditionsの時と同じだよ。

q = {
  "g" => {
    # グループ1
    "0" => {
      "c" => {
        "0" => {
          "a" => { "0" => { "name" => "first_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "太郎" } }
        }
      }
    },
    # グループ2
    "1" => {
      "c" => {
        "0" => {
          "a" => { "0" => { "name" => "last_name" } },
          "p" => "eq",
          "v" => { "0" => { "value" => "田中" } }
        }
      }
    }
  },
  "m" => "and"
}

User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`first_name` = '太郎' AND `users`.`last_name` = '田中')"

combinator関係のメソッド

g.combinator_select - アドバンストモードでcombinatorのセレクトボックスを用意する

# combinatorのセレクトボックス。
# any(or)とall(and)の2つを選択できる。
<%= c.combinator_select %>

040 s(sorts) - ソート

🐱 sパラメーターを使うことで、ソートを実現できるよ。

# シンプルモード
User.ransack(s: "id DESC").result.to_sql
=> "SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC"

# アドバンストモード
q = {
 "s" => {
   "0" => {
      "name" => "id", # カラム名
      "dir" => "desc" # direction(asc/desc)。指定なしの場合はasc。
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC"

ソートのセレクトボックスを用意する

🐱 こんな感じで、ソートする属性とasc/descをセレクトボックスで指定できるような画面を作成するよ。

f:id:nekorails:20170530000539p:plain

🐱 SQLはこんな感じだよ。

q = {
 "s" => {
   "0" => {
      "name" => "id", # カラム名
      "dir" => "asc" # direction(asc/desc)。指定なしの場合はasc。
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC"

🐱 ビューはこんな感じだよ。

<%= search_form_for @search do |f| %>
  # params[:q][:s]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
  <%= f.sort_fields do |s| %>
    # 属性のセレクトボックスと、asc/descのセレクトボックスをの2つを用意してくれる
    <%= s.sort_select %>
  <% end %>

  <%= f.submit %>
<% end %>

🐱 コントローラーはこんな感じだよ。

def index
  @search = User.ransack(params[:q])
  # 初期状態ではsortは1つも存在しないので、空のsortを1つ作る。
  # ここでsortを作っておかないと、f.sort_fieldsでループするsortが1つもなくて、画面に何も表示されない。
  @search.build_sort if @search.sorts.empty?

  @users = @search.result
end

ソートのセレクトボックスを2つ用意する

🐱 こんな感じで、属性とasc/descをセレクトボックスを2セット用意するよ。

f:id:nekorails:20170530000543p:plain

🐱 SQLはこんな感じだよ。

q = {
 "s" => {
   "0" => {
      "name" => "first_name",
      "dir" => "asc"
    },
   "1" => {
      "name" => "last_name",
      "dir" => "desc"
    }
  }
}
User.ransack(q).result.to_sql
=> "SELECT `users`.* FROM `users` ORDER BY `users`.`first_name` ASC, `users`.`last_name` DESC"

🐱 ビューはこんな感じだよ。1つ用意する場合と同じだね。

<%= search_form_for @search do |f| %>
  # params[:q][:s]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
  <%= f.sort_fields do |s| %>
    # 属性のセレクトボックスと、asc/descのセレクトボックスをの2つを用意してくれる
    <%= s.sort_select %>
  <% end %>

  <%= f.submit %>
<% end %>

🐱 コントローラーはこんな感じだよ。

def index
  @search = User.ransack(params[:q])
  # sortが存在しない場合は、空のsortを作っておく。
  2.times { |i| @search.build_sort unless @search.sorts[i] }

  @users = @search.result
end

ソートのリンクを用意する

🐱 テーブルのヘッダ列に対してsort_linkメソッドを使ってあげればソートのリンクになるよ。

# 検索結果
<table>
  <tr>
    <th><%= sort_link(@search, :id) %></th>
    <th><%= sort_link(@search, :first_name) %></th>
    <th><%= sort_link(@search, :last_name) %></th>
  </tr>

  <% @users.each do |user| %>
    <tr>
      <td><%= user.id %></td>
      <td><%= user.first_name %></td>
      <td><%= user.last_name %></td>
    </tr>
  <% end %>
</table>

🐱 これだけでソート機能の完成だよ。

f:id:nekorails:20170530000546p:plain

カスタムのソートのリンクを用意する

🐱 ransackerを使って仮想属性を用意してあげれば、カスタムのソートが可能だよ。ransackerについて詳しく知りたい場合は、第6章 ransackerで検索するを参照してね。

# user.rb
ransacker :full_name { Arel.sql('CONCAT(first_name, " ", last_name)') }
# index.html.erb
sort_link(@search, :full_name)

🐱 SQLと画面はこんな感じになるよ。

SELECT `users`.* FROM `users` ORDER BY CONCAT(first_name, " ", last_name) ASC

f:id:nekorails:20170530000550p:plain

デフォルトのソートを指定する

# users_controller.rb
def index
  @search = User.ransack(params[:q])
  # 初期状態ではsortは存在しないので、sortを作る。
  @search.sorts = 'id asc' if @search.sorts.empty?

  @users = @search.result
end

f:id:nekorails:20170530000554p:plain

複数指定する

# 配列で複数指定可能
@search.sorts = ['first_name asc', 'last_name desc'] if @search.sorts.empty?

🐱 SQLはこんな感じになるよ

SELECT `users`.* FROM `users` ORDER BY `users`.`first_name` ASC, `users`.`last_name` DESC

f:id:nekorails:20170530000557p:plain

sort関係のメソッド

f.sort_fields - アドバンストモードでsに対応するフィールドを用意する

# params[:q][:s]["0"]に対応するよ("0"はコレクションの連番の1つ目を表すよ。)
<%= f.sort_fields do |s| %>
  <%= s.sort_select %>
<% end %>

f.sort_select - 属性のセレクトボックスと、asc/descのセレクトボックスをの2つを用意してくれる

<%= f.sort_fields do |s| %>
  # 属性のセレクトボックスと、asc/descのセレクトボックスをの2つを用意してくれる
  <%= s.sort_select %>
<% end %>

search.sorts - sortを取得する

search.sorts
=> [Sortインスタンス]

# alias
search.s
=> [Sortインスタンス]

search.sorts= - sortをセットする

# ORDER BY `users`.`id` ASC
search.sorts = 'id asc'

# ascは省略可能
# ORDER BY `users`.`id` ASC
search.sorts = 'id'

# alias
# ORDER BY `users`.`id` ASC
search.s = 'id'

# 配列で複数指定可能
# ORDER BY `users`.`first_name` ASC, `users`.`last_name` DESC
search.sorts = ['first_name asc', 'last_name desc']
# 基本
sort_link(@search, :first_name)

# テキストを指定
sort_link(@search, :first_name, "名前")

# タグを指定(link_toメソッドと同じだね。)
<%= sort_link(@search, :name) do %>
  <strong>名前</strong>
<% end %>

# 関連先のカラムでソート
# user has_one roleの場合
sort_link(@search, :role_name)

# 複数カラムでソート
# first name -> last_name descの順でソート
sort_link(@search, :first_name, ['first_name asc', 'last_name desc'])

# ソートの矢印を非表示
sort_link(@search, :first_name, hide_indicator: true)

# デフォルトを降順にする
sort_link(@search, :first_name, default_order: :desc)

第4章 scopeで検索する

041 scopeで検索する

🐱 ぼく太くん、scopeは知ってる?

👦🏻 んー、検索条件を定義しておけるやつだっけ?

🐱 そうだよ。これがRansackから使えたら便利だと思わない?実際にRansackを使ってみると、シンプルモードだと痒いところに手が届かない場合があるんだよね。そんな時にscopeが使えるとすごく便利なんだよ。Ransackではscopeを定義して、self.ransackable_scopesをオーバーライドすることでscopeを使えるようになるんだよ。

class Employee < ApplicationRecord
  # scopeを定義する。
  scope :activated, -> { where(active: true) }

  # Ransackで使うscopeを指定する。
  # 戻り値はシンボルの配列を使う。
  # デフォルトでは全てのscopeは認可されていない。
  def self.ransackable_scopes(auth_object = nil)
    %i(activated)
  end
end

🐱 SQLはこんな感じになるよ。

Employee.ransack(activated: true).result.to_sql
=> "SELECT `employees`.* FROM `employees` WHERE `employees`.`active` = 1"

🐱 ビューはこんな感じだよ。scopeの場合は述語は必要ないよ。

f.check_box :activated

042 booleanへの変換に注意する

🐱 Ransackでは、1や’T'などの値は真っぽい値として変換されてしまうんだ(参照:030 真っぽい値・偽っぽい値)。そのため、そのまま1などの値を扱うとエラーになっちゃうんだ。

# scopeを定義
class Employee < ApplicationRecord
  scope :age_greater_than, -> (age) { where('age > ?', age) }

  def self.ransackable_scopes(auth_object = nil)
    %i(age_greater_than)
  end
end

# 成功
Employee.ransack(age_greater_than: 2).result.to_sql
=> "SELECT `employees`.* FROM `employees` WHERE (age > 2)"

# 失敗
Employee.ransack(age_greater_than: 1).result.to_sql
=> ArgumentError: wrong number of arguments (given 0, expected 1)

🐱 こんな時はsanitize_custom_scope_booleansオプションをfalseに変更してあげれば、変換を止められるよ。

# config/initializers/ransack.rb
# 変換しないようにする
Ransack.configure do |config|
  config.sanitize_custom_scope_booleans = false
end

# 成功
Employee.ransack(age_greater_than: 1).result.to_sql
=> "SELECT `employees`.* FROM `employees` WHERE (age > 1)"

🐱 sanitize_custom_scope_booleansは最近追加されたオプションだから、使いたい場合は最新バージョンにしてあげてね。

第5章 カスタム述語で検索する

043 カスタム述語とは?

🐱 Ransackでは述語(Predicate)を自分で定義することが可能なんだ。こんな感じで定義するんだよ。

# config/initializers/ransack.rb

Ransack.configure do |config|
  config.add_predicate 'equals_diddly', # 述語名
                       # Arelの述語(eqとか)
                       # Arelについては 第9章 RansackのためのArel入門 を参照してね。
                       arel_predicate: 'eq',
                       # 入力値の整形
                       # デフォルトは何もしないよ。
                       formatter: proc { |v| "#{v}-diddly" },
                       # 入力値のバリデーション。戻り値がfalseの場合はこの条件は無視される。
                       # デフォルトはpresent?でバリデートするよ。
                       validator: proc { |v| v.present? },
                       # 述語allと述語anyを使えるようにする。
                       # デフォルトはtrue
                       compounds: true,
                       # 入力値の型変換
                       # デフォルトはDBのカラムの型を利用する。
                       type: :string
end

👦🏻 さっぱりわからない( ´∀`)

🐱 そうだね。これだけだとわかりにくいから、次ページ以降で実際に使えるレシピを紹介していくよ。

044 datetime型カラムをdate型で検索する

🐱 datetime型のカラムに対して2017-04-01のようなdate型で範囲検索したくなる場合ってあるよね?

👦🏻 まぁね。

🐱 そういう場合にはその日の終わり(23:59:59)までを検索対象に含めたいでしょ?そのためには入力値を整形する必要があるんだ。カスタム述語を使えばこれがスマートにできるんだよ。

👦🏻 ええやん。

🐱 (僕の方が先輩なのに…)。まずはカスタム述語を定義するよ。config/initializers/ransack.rbが存在しない場合は追加してね。

# config/initializers/ransack.rb
Ransack.configure do |config|
                       # 述語名
  config.add_predicate 'lteq_end_of_day',
                       # Arelの述語を指定。<=で検索したいからlteqを使うよ。
                       arel_predicate: 'lteq',
                       # インプットの整形。その日の終わりまでを検索対象に含めるよ。
                       formatter: proc { |v| v.end_of_day }
end

🐱 この述語を使うとSQLはこんな感じになるよ。'2017-04-01'までに作成されたProductを検索してるよ。

Product.ransack(created_at_lteq_end_of_day: '2017-04-01').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`created_at` <= '2017-04-01 23:59:59')"

🐱 ビューはこんな感じだよ。

f.search_field :created_at_lteq_end_of_day

045 半角スペース区切りの文字列で検索する

🐱 “ディスプレイ 43型” みたいな入力を受け取って、各文字列を含むレコードを検索したい場合あるよね?そんな時にもカスタム述語は使えるよ。

🐱 述語の定義はこんな感じだよ。

# config/initializers/ransack.rb
Ransack.configure do |config|
                       # 述語名
  config.add_predicate 'has_every_term',
                       # Arelの述語を指定。全ての要素に対してLIKE検索したいからatches_allを使うよ。
                       arel_predicate: 'matches_all',
                       # インプットの整形。半角スペース区切りの文字列を、配列にして部分一致するようにしてるよ。
                       formatter: proc { |v| v.split.map { |t| "%#{t}%" } }
end

🐱 この述語を使うとSQLはこんな感じになるよ。

Product.ransack(name_has_every_term: "ディスプレイ 43型").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`name` LIKE '%ディスプレイ%' AND `products`.`name` LIKE '%43型%')"

🐱 ビューはこんな感じだよ。

f.search_field :name_has_every_term

第6章 ransackerで検索する

046 ransakerとは?

👦🏻 ねぇ、猫先輩。WHERE句がname = 'ほげ' の場合に、=の部分はカスタム述語を使えば変えられるでしょ?nameの部分を変えることってできないのかな?

🐱 ransackerを使えばできるよ。ransackerはモデルにこんな感じで定義するよ。

ransacker name, options

🐱 nameは仮想属性の名前だよ。この仮想属性を通して、定義したransackerを利用することになるよ。optionsは以下の通りだよ。

オプション 解説
callable proc { Arel.sql(‘DATE(created_at)’) } WHERE句の左辺(name)を変形する。デフォルトは何もしない。オプションではなくブロックでも指定可能
formatter proc { |v| v.reverse } WHERE句の右辺(‘ほげ’)を整形する。デフォルトは何もしない。
type :string WHERE句の右辺(‘ほげ’)の型を指定する。デフォルトはString
args [:parent] callableのprocに渡す引数。デフォルトは[:parent]。Arelを使ってSQLを組み立てる場合は、parent.tableでArel::TableオブジェクトにアクセスしてSQLを組み立てる。

🐱 ransackerを使いこなすにはArelというgemの知識が必要になるよ。とりあえずは、Arel.sql()で生SQLをRansack(の内部で使われているArel)で扱える形にしてるってことだけ覚えておけば大丈夫だよ。Arelについて詳しく知りたい場合は、第9章 RansackのためのArel入門を参照してね。

🐱 次ページから実際に使えるレシピを紹介していくよ。

047 datetime型カラムをdate型で検索する

🐱 ransackerをモデルに定義してね。。Arel.sql()は生SQLをRansack(の内部で使われているArel)で扱える形にしてるよ。

class Product < ApplicationRecord
  ransacker :created_at, callable: proc { Arel.sql('DATE(created_at)') }
end

🐱 SQLはこんな感じになるよ。created_atDATE(created_at)になっていることを確認してね。

Product.ransack(created_at_eq: '2017-05-03').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(created_at) = '2017-05-03'"

🐱 ビューはこんな感じだよ。

f.search_field :created_at_eq

🐱 ちなみにcallableオプションはブロックでも書けるよ。formatterオプションとかがない場合は、コッチの方が読みやすいね。

class Product < ApplicationRecord
  # ransacker :created_at, callable: proc { Arel.sql('DATE(created_at)') }
  ransacker :created_at { Arel.sql('DATE(created_at)') }
end

048 Integer型カラムをLIKE検索する

👦🏻 idに対してLIKE検索をしてもうまくいかないよー。

Product.ransack(id_cont: 1).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`id` LIKE 0)"

🐱 そんな場合もransackerを使ってね。MySQLだとこんな感じになるよ。

# product.rb
ransacker :id { Arel.sql("CONVERT(#{table_name}.id, CHAR(8))") }

🐱 SQLはこんな感じになるよ。

Product.ransack(id_cont: 1).result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (CONVERT(products.id, CHAR(8)) LIKE '%1%')"

🐱 ビューはこんな感じだよ。

f.search_field :id_cont

049 full_nameを検索する

🐱 last_name(名字)first_name(名前)というカラムがあると想定してね。full_name(フルネーム)という仮想のカラムに対して検索するにはこんな感じになるよ。CONCAT()は文字列を結合するSQL関数だよ。

# user.rb
ransacker :full_name { Arel.sql('CONCAT(last_name, first_name)') }

🐱 SQLはこんな感じになるよ。

User.ransack(full_name_eq: "山田太郎").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE CONCAT(last_name, first_name) = '山田太郎'"

🐱 ビューはこんな感じだよ。

f.search_field :full_name_eq

🐱 もちろんeq述語以外も利用できるよ。cont述語だとこんな感じだよ。

User.ransack(full_name_cont: "").result.to_sql
=> "SELECT `users`.* FROM `users` WHERE (CONCAT(last_name, first_name) LIKE '%山%')"

🐱 ビューはこんな感じだよ。

f.search_field :full_name_cont

050 String型カラムを反転して検索する

左辺を反転して検索する

🐱 ransackerはこんな感じになるよ。REVERSE()は文字列を反転させるSQL関数だよ。

# product.rb
ransacker :reversed_name { Arel.sql('REVERSE(name)')}

🐱 SQLはこんな感じだよ。

Product.ransack(reversed_name_eq: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE REVERSE(name) = 'ほげ'"

🐱 ビューはこんな感じだよ。

f.search_field :reversed_name_eq

右辺を反転して検索する

🐱 formatterオプションを使って、WHERE句の右辺を反転させる方法もあるよ。

# product.rb

ransacker :reversed_name,
          # 右辺
          formatter: proc { |v| v.reverse },
          # 左辺
          # nameを指定して置かないと、左辺がreversed_nameになってしまうよ。
          callable: proc { Arel.sql('name') }

🐱 SQLはこんな感じだよ。

Product.ransack(reversed_name_eq: "ほげ").result.to_sql
=> "SELECT `products`.* FROM `products` WHERE name = 'げほ'"

051 ransackerのコツ

生SQLはテーブル名.カラム名で指定する

🐱 生SQLを書く時はカラム名と書くよりも、テーブル名.カラム名と書いた方が安心だよ。前者の場合は、JOINした時に別のテーブルに同じカラム名があるとエラーになっちゃうからね。ransackerのコツというよりも、生SQLを扱う際のコツだね。

# 単独で使う場合は問題ない。
ransacker :created_at { Arel.sql('DATE(created_at)') }

# JOINしても安心。
ransacker :created_at { Arel.sql('DATE(products.created_at)') }

🐱 そもそも生SQLを使わずに、Arelを使うっていう方法もあるよ。Arelについて詳しく知りたい場合は、第9章 RansackのためのArel入門を参照してね。

# モデル
ransacker :created_at { |parent| Arel::Nodes::NamedFunction.new('DATE', [parent.table[:name]]) }

# 使用
Product.ransack(created_at_eq: '2017-05-11').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(`products`.`name`) = '2017-05-11'"

仮想属性に別名をつける

🐱 元のカラムと同名で定義しちゃうと、元の方法で検索できなくなっちゃうよ。

# モデル
ransacker :created_at { Arel.sql('DATE(created_at)') }

# 使用
# date型で検索するようにしたので、元のdatetime型ではうまく検索できない。
Product.ransack(created_at_eq: '2017-05-11 06:14:51').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(created_at) = '2017-05-11 06:14:51'"

🐱 そんな時は仮想属性の名前を別に用意してあげるといいよ。

# モデル
ransacker :created_on { Arel.sql('DATE(created_at)') }

# 使用
# date型の場合は`created_on`を使う
Product.ransack(created_on_eq: '2017-04-01').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(created_at) = '2017-04-01'"

# datetime型の場合は`created_at`を使う
Product.ransack(created_at_eq: '2017-05-11 06:14:51').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE `products`.`created_at` = '2017-05-11 06:14:51'"

第7章 4つの認可

052 認可とは?

🐱 アドバンストモードのattribute_selectで指定の属性しか選べないようにしたい時があるよね?そんな時のために認可(Authorization)の概念を覚えておいてね。Ransackでは以下の4つのメソッドで認可を指定できるよ。

クラスメソッド 対象 デフォルト
ransackable_attributes 属性 全ての属性を認可(ransackerで定義した仮想属性を含む)
ransortable_attributes ソート 全ての属性を認可(ransackerで定義した仮想属性を含む)
ransackable_associations 関連 全ての関連を認可
ransackable_scopes scope 全てのscopeが認可されていない

🐱 ActiveRecord::Baseにこの4つのクラスメソッドが定義されているよ。これらをオーバーライドして、認可する属性を自分で指定することができるんだ。認可する対象を文字列配列で返してあげてね(scopeはシンボル配列)。例えば認可する属性をfirst_nameとlast_nameだけにする場合はこんな感じだよ。

# user.rb

class User < ApplicationRecord
  private

  def self.ransackable_attributes(auth_object = nil)
    %w(first_name last_name)
  end
end

🐱 idは認可されていないため、条件に使えないよ。

User.ransack(id_eq: 1).result.to_sql
=> "SELECT `users`.* FROM `users`"

🐱 attribute_selectではfirst_nameとlast_nameしか選べなくなっているよ。

f:id:nekorails:20170530000601p:plain)

053 ユーザーによって認可対象を変える

🐱 これら4つのメソッドはauth_objectを使うことで、ユーザー毎に認可対象を変えることができるよ。例えばadminユーザーは全ての属性にアクセスできるようにして、一般ユーザーはfirst_nameとlast_nameにしかアクセスできないようにできるよ。

# user.rb

class User < ApplicationRecord
  private

  # auth_objectによって、認可対象が変わるよ
  def self.ransackable_attributes(auth_object = nil)
    auth_object == :admin ? super : %w(first_name last_name)
  end
end
# users_controller.rb

class UsersController < ApplicationController
  def index
    # current_userによってauth_objectが変わるよ。
    @search = User.ransack(params[:q], auth_object: auth_object)
    @articles = @search.result
  end

  private

  def auth_object
    current_user.admin? ? :admin : nil
  end
end
# 一般ユーザーはこの2つしかアクセスできない
User.ransackable_attributes
=> ["first_name", "last_name"]

# 一般ユーザーはidにはアクセスできない
User.ransack(id_eq: 1).result.to_sql
=> "SELECT `users`.* FROM `users`"

# adminユーザーは全属性にアクセスできる
User.ransackable_attributes(:admin)
=> ["id", "first_name", "last_name", "email", "password_digest", "created_at", "updated_at", "name", "full_name"]

# adminユーザーはidにもアクセスできる
User.ransack({id_eq: 1}, {auth_object: :admin}).result.to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 1"

第8章 設定

054 Ransackを設定する

🐱 Ransackはconfig/initializers/ransack.rbで、こんな感じで設定するといいよ。

Ransack.configure do |config|
  # デフォルトのサーチパラメーターのキーを、qからqueryに変更する
  config.search_key = :query
end

055 デフォルトのサーチパラメーターのキーを変更する(search_key)

# デフォルトは:q
# サーチパラメーターのキーを、queryに変更する
config.search_key = :query

056 知らない述語・属性を無視する(ignore_unknown_conditions)

# デフォルトはtrue(無視する)
# falseにすると例外を投げる
config.ignore_unknown_conditions = false

057 ソートの矢印を隠す(hide_sort_porder_indicators)

# デフォルトはfalse(隠さない)
config.hide_sort_porder_indicators = true

🐱 個別に隠すことも可能だよ。

sort_link(@search, :first_name, hide_indicator: true)

058 ソートの矢印の見た目を変える(custom_arrows)

config.custom_arrows = {
  # 上向きの矢印
  # HTMLで指定可能
  up_arrow: '<i class="custom-up-arrow-icon"></i>',
  # 下向きの矢印
  down_arrow: 'U+02193'
}

059 scopeのbooleanのサニタイズ(sanitize_custom_scope_booleans)

# デフォルトはtrue(サニタイズする)
# falseにするとサニタイズしない。
config.sanitize_custom_scope_booleans = false
# falseにすればエラーにならないよ。
Employee.ransack(age_greater_than: 1).result.to_sql
=> "SELECT `employees`.* FROM `employees` WHERE (age > 1)"

第9章 RansackのためのArel入門

060 Arelとは?

👦🏻 ところで、Arelってなに?

🐱 SQLを生成するgemだよ。RailsのActiveRecordの内部でも使用されているんだ。こんな感じでSQLを生成できるよ。

Arel::Table.new('users').project('id').to_sql
=> "SELECT id FROM `users`"

🐱 RansackではSQLを生成するのに、内部的にArelを利用しているんだ。だから、Ransackで応用的な機能を使う際には、Arelの知識がちょっとだけ必要になるんだよ。この章ではRansackで必要になるArelの知識をかいつまんで説明していくね。

061 Arelの基本

🐱 Arelはこんな感じで使えるよ。

# Arel::Table.newで作成したtableオブジェクトは、作成されるSQLの`FROM products`部分に対応してるよ。
products = Arel::Table.new(:products)

# projectは`SELECT id`に対応
# to_sqlはSQL文字列に変換
products.project('id').to_sql
=> "SELECT id FROM `products`"

# whereは`WHERE`に対応
# products[:id].eq(1)は`products.id = 1`に対応
# products[:id]はカラム`products.id`に対応
# eqは述語`=`に対応
products.project('id').where(products[:id].eq(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` = 1"

# 述語はRansackとほとんど同じ
products.project('id').where(products[:id].eq(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` = 1"
products.project('id').where(products[:id].gt(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` > 1"
products.project('id').where(products[:id].lt(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` < 1"
products.project('id').where(products[:id].gteq(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` >= 1"
products.project('id').where(products[:id].lteq(1)).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` <= 1"
products.project('id').where(products[:id].in([1, 2])).to_sql
=> "SELECT id FROM `products` WHERE `products`.`id` IN (1, 2)"
products.project('id').where(products[:name].matches("%ほげ%")).to_sql
=> "SELECT id FROM `products` WHERE `products`.`name` LIKE '%ほげ%'"


# OR検索なども可能
products.project('id').where(products[:id].eq(1).or products[:id].eq(2)).to_sql
=> "SELECT id FROM `products` WHERE (`products`.`id` = 1 OR `products`.`id` = 2)"

062 ArelとActiveRecord

🐱 ActiveRecordでは、SQLを生成するのに内部的にArelを使っているよ。だからActiveRecordではこんな感じでArelを使ってSQLを組み立てることが可能だよ。

# Arel::Tableのインスタンスを取得
users = User.arel_table

# Arelを使ってSQLを組み立てられる。
User.where(users[:id].gt(1)).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`id` > 1)"

🐱 生SQLの代わりにArelを使えば、DBに依存しない形でSQLを組み立てることができるよ。ただ、生SQLで書いた方がわかりやすいことが多いので、無理してArelを使わずに生SQLで書いてしまってもいいのかなー、とも思うよ。

063 ArelとRansack

🐱 Ransackでは、SQLを生成するのに内部的にArelを使っているよ。基本的にはArelについて知らなくてもRansackは使えるんだけど、以下の2つの機能を使う際にはちょっとだけArelの知識が必要になるよ。

  • カスタム述語
  • ransacker

カスタム述語とArel

🐱 カスタム述語は自分で述語を定義できる機能だよ。

# カスタム述語を追加
Ransack.configure do |config|
                       # 述語名
  config.add_predicate 'lteq_end_of_day',
                       # Arelの述語を指定。<=で検索したいからlteqを使うよ。
                       arel_predicate: 'lteq',
                       # インプットの整形。ここがポイント。その日の終わりまでを検索対象に含めるよ。
                       formatter: proc { |v| v.end_of_day }
end

# 使用
Product.ransack(created_at_lteq_end_of_day: '2017-04-01').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE (`products`.`created_at` <= '2017-04-01 23:59:59')"

🐱 arel_predicateオプションでArelの述語を指定する際にArelの知識が必要になるよ。Arelの述語はこんな感じになるから、使えそうな述語を選んで指定してね。

述語名 述語 Arelの例 SQL
eq = users[:id].eq(1) users.id = 1
gt > users[:id].gt(1) users.id > 1
lt < users[:id].lt(1) users.id < 1
gteq >= users[:id].gteq(1) users.id >= 1
lteq <= users[:id].lteq(1) users.id <= 1
in IN users[:id].in([1, 2]) users.id IN (1, 2)
between BETWEEN users[:id].between(1..3) users.id BETWEEN 1 AND 3
match LIKE users[:name].matches(“%ほげ%”) users.name LIKE ‘%ほげ%’
*_all AND users[:id].eq_all([1, 2]) (users.id = 1 AND users.id = 2)
*_any OR users[:id].eq_any([1, 2]) (users.id = 1 OR users.id = 2)
not_* NOT users[:id].not_eq(1) users.id != 1

ransackerとArel

🐱 ransackerは仮想属性を定義して、カスタムの検索をできるようにする機能だよ。ブロックではeqのような述語メソッドがチェーンできるようなオブジェクトを返す必要があるよ。Arel.sqlを使えば、生SQLをArelで扱えるクラスにかえられるよ。

# Arel.sqlを使えばeqでメソッドチェーンできる
Arel.sql('DATE(created_at)').eq('2017-05-03').to_sql
=> "DATE(created_at) = '2017-05-03'"

# ransackerの定義
class Product < ApplicationRecord
  ransacker :created_at { Arel.sql('DATE(created_at)') }
end

# ransackerの使用
Product.ransack(created_at_eq: '2017-05-03').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(created_at) = '2017-05-03'"

🐱 ransackerではparentというブロック引数を取れるよ。parent.tableとすることでArel::Tableのインスタンスにアクセスできるから、parent.table[:name]などを使ってArelを組み立てることもできるよ。Arel::Nodes::NamedFunction.newはSQL関数を作るよ。

# ransackerの定義
class Product < ApplicationRecord
  ransacker :created_at { |parent| Arel::Nodes::NamedFunction.new('DATE', [parent.table[:name]]) }
end

# ransackerの使用
Product.ransack(created_at_eq: '2017-05-03').result.to_sql
=> "SELECT `products`.* FROM `products` WHERE DATE(`products`.`name`) = '2017-05-03'"

第10章 Tips詰め合わせ

064 重複を取り除く

# distinctオプションを使えば`SELECT DISTINCT`で検索できるよ。
User.ransack(params[:q]).result(distinct: true)

# 普通にRelationに対してdistinctしてもOKだよ。
User.ransack(params[:q]).result.distinct

065 デフォルト条件をセットする

🐱 こんな感じでsearchオブジェクトに直接セットしてあげるといいよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q])
  # デフォルトの条件
  # 最初のリクエスト時だけセットしてあげる。
  @search.id_eq = 1 unless params[:q]

  @users = @search.result
end

066 常にOR検索する

🐱 RansackはデフォルトではAND検索をするよ。OR検索をしたい場合は、こんな感じでsearchオブジェクトのcombinatorに直接セットしてあげるといいよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q])
  @search.combinator = 'or'

  @users = @search.result
end

🐱 paramsを加工してあげてもいいよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q].try(:merge, m: 'or'))
  @users = @search.result
end

067 フォームを2つ用意する

🐱 Ransackでフォームを二つ用意すると、どちらもキーが:qになってかぶっちゃうよ。だからransackメソッドのsearch_keyオプションを使って、キーを変えてあげてね。

def index
  # キーは:q
  @search = Product.ransack(params[:q])
  @products = @search.result

  # キーは:log_search
  @log_search = Log.ransack(params[:log_search], search_key: :log_search)
  @logs = @log_search.result
end
<%= f.search_form_for @search do |f| %>
# ...略...

<%= f.search_form_for @log_search, as: :log_search do |f| %>
# ...略...

068 searchメソッド

🐱 ransackメソッドにはsearchというエイリアスがあるよ。

# この2つは同じ
User.ransack(params[:q])
User.search(params[:q])

🐱 ただ、searchというメソッド名はすごく一般的だから、他のgemとコンフリクトしちゃうことがあるんだよね。将来的にはsearchメソッドはdeprecatedになる予定なので、ransackメソッドを使うことをオススメするよ。

他のgemとコンフリクトした場合

🐱 searchメソッドが他のgemとコンフリクトした場合は、Ransackのsearchメソッドを削除してね。

# config/initializers/ransack.rb:

Ransack::Adapters::ActiveRecord::Base.class_eval('remove_method :search')

069 属性名にエイリアスを付ける(ransack_alias)

🐱 ransack_aliasメソッドを使うと、属性名にエイリアスを付けられるよ。関連などを含む長い属性名に対してエイリアスをつけておくと便利だよ。

class Post < ApplicationRecord
  belongs_to :author

  # authorというエイリアスをつける
  ransack_alias :author, :author_first_name_or_author_last_name
end
<%= search_form_for @search do |f| %>
  # authorでアクセスできる
  <%= f.label :author_cont %>
  <%= f.search_field :author_cont %>
<% end %>

070 Ransackの日本語化

🐱 下記のファイルをconfig/locales/ransack.ja.ymlに置けば日本語化できるよ。

ja:
  ransack:
    all: "全て"
    and: "かつ"
    any: "いずれかの"
    asc: "昇順"
    attribute: "属性"
    combinator: "結合子"
    condition: "条件"
    desc: "降順"
    or: "または"
    predicate: "述語"
    predicates:
      blank: "空である"
      cont: "含む"
      cont_all: "全て含む"
      cont_any: "いずれかを含む"
      does_not_match: "マッチしない"
      does_not_match_all: "全てマッチしない"
      does_not_match_any: "いずれかにマッチしない"
      end: "で終わる"
      end_all:
      end_any: "いずれかで終わる"
      eq: "等しい"
      eq_all: "全て等しい"
      eq_any: "いずれかが等しい"
      'false': "が false である"
      gt: "より大きい"
      gt_all: "全てより大きい"
      gt_any: "いずれかより大きい"
      gteq: "以上"
      gteq_all: "全て以下"
      gteq_any: "いずれかが以下"
      in: "含む"
      in_all: "全てを含む"
      in_any: "いずれかを含む"
      lt: "より小さい"
      lt_all: "全てより小さい"
      lt_any: "いずれかより小さい"
      lteq: "以下"
      lteq_all: "全て以下"
      lteq_any: "いずれかが以下"
      matches: "とマッチする"
      matches_all: "全てに一致する"
      matches_any: "いずれかに一致する"
      not_cont: "含まない"
      not_cont_all: "いずれも含まない"
      not_cont_any: "いずれかを含まない"
      not_end: "で終わらない"
      not_end_all: "全てで終わらない"
      not_end_any: "いずれかで終わらない"
      not_eq: "等しくない"
      not_eq_all: "全てと等しくない"
      not_eq_any: "いずれかと等しくない"
      not_in: "含まない"
      not_in_all:
      not_in_any:
      not_null: null ではない
      not_start: "で始まらない"
      not_start_all:
      not_start_any:
      'null': null ではない
      present: "存在する"
      start: "ではじまる"
      start_all:
      start_any:
      'true': true である
    search: "検索する"
    sort: "ソートする"
    value: "値"

🐱 ログインが必要だけど、このサイトからダウンロードすることもできるよ。 https://www.localeapp.com/projects/2999/downloads

071 searchアクションにPOSTする

🐱 アドバンストモードだとパラメーターのサイズが大きくなるから、GETメソッドのサイズ制限に引っかかってしまう場合があるよ。そんな時には代わりにPOSTメソッドを使ってね。このレシピではsearchアクションを用意して、そこにPOSTしているよ。

# routes.rb

# searchアクションにPOSTできるようにするよ。
resources :people do
  collection do
    match 'search' => 'people#search', via: [:get, :post], as: :search
  end
end
# people_controller.rb

def search
  # 処理はindexと共有するよ。
  index
  render :index
end
# index.html.erb

# 検索はsearchアクションに対してPOSTするよ。
<%= search_form_for @search, url: search_people_path, html: { method: :post } do |f| %>

072 Kaminariと一緒に使う

🐱 Kaminariはページネーション機能を提供してくれるよ。

# users_controller.rb

def index
  @users = User.page(params[:page])
end

🐱 Ransackと一緒に使う場合は、@search.resultに対してpageをメソッドチェーンしてあげればOKだよ。

# users_controller.rb

def index
  @search = User.ransack(params[:q])
  @users = @search.result.page(params[:page])
end

073 Mongoidと一緒に使う

🐱 MongoidはMongoDB用のODM(Object-Document-Mapper)だよ。RansackはMongoidに対応しているから、ActiveRecordと同じように使えるよ。

@search = User.ransack(params[:q])

付録

Ransackの情報源

日本語の情報源

RailsCasts - Ransack

http://railscasts.com/episodes/370-ransack?language=ja&view=asciicast

🐱 Railsのスクリーンキャストを配信しているサイトだよ。スクリーンキャストは英語だけど、書き起こしが日本語に翻訳されているよ。アドバンストモードでJavaScriptを使って動的にフィールドを増減する方法などが載っているよ。

おもしろwebサービス開発日記 - ransack という検索用の gem について

http://blog.willnet.in/entry/2013/04/09/115216

🐱 パーフェクトRails著者のwillnetさんのブログだよ。カスタム述語の使い方などが載っているよ。

Qiita - Ransackのススメ

http://qiita.com/nysalor/items/9a95d91f2b97a08b96b0

🐱 Ransackの基本的な使い方が日本語で紹介されているよ。

英語の情報源

readme

https://github.com/activerecord-hackery/ransack/blob/master/README.md

🐱 基本的な使い方が載っているよ。

githubのwiki

https://github.com/activerecord-hackery/ransack/wiki

🐱 述語やransackerの情報などは、readmeより詳しく載っているよ。

Ransackのデモサイト

デモサイト:http://ransack-demo.herokuapp.com/

ソースコード:https://github.com/activerecord-hackery/ransack_demo

🐱 シンプルモードとアドバンストモードのデモサイトだよ。アドバンストモードはJavaScriptで動的に増減可能になっていて、ユーザーが自由にSQLを組み立てられるようになっているよ。

Ransackのソースコード

https://github.com/activerecord-hackery/ransack

🐱 アドバンストモードなどの応用的な機能のドキュメントはあまり多くないので、直接コードを見た方が早い場合もあるよ。

LocalApp - Ransack

https://www.localeapp.com/projects/2999/downloads

🐱 ログインが必要だけど、このサイトから日本語のロケールファイルをダウンロードできるよ。

Kindle Unlimited版の紹介

🐱 Kindle Unlimited版もあるよ。内容は同じだけど多少読みやすくなってるよ。Kindle Unlimitedで無料で読めるよ。