Ransackで簡単に検索フォームを作る73のレシピ
- はじめに
- 第1章 Ransackをはじめよう
- 第2章 シンプルモードで検索する
- 004 シンプルモードとは?
- 005 述語とは?
- 006 eq - =検索
- 007 matches - LIKE検索
- 008 cont - LIKE検索(部分一致)
- 009 start - LIKE検索(前方一致)
- 010 end - LIKE検索(後方一致)
- 011 gt - >検索
- 012 gteq - >=検索
- 013 lt - <検索
- 014 lteq - <=検索
- 015 true - trueの検索
- 016 false - falseの検索
- 017 blank - blank?の検索
- 018 present - present?の検索
- 019 null - NULLの検索
- 020 in - IN検索
- 021 not - 否定
- 022 all - ANDで繋ぐ(値が複数)
- 023 any - ORで繋ぐ(値が複数)
- 024 and - ANDで繋ぐ(カラムが複数)
- 025 or - ORで繋ぐ(カラムが複数)
- 026 関連
- 027 条件を組み合わせる
- 028 範囲検索
- 029 カラムが存在しない場合
- 030 真っぽい値・偽っぽい値
- 第3章 アドバンストモードで検索する
- 第4章 scopeで検索する
- 第5章 カスタム述語で検索する
- 第6章 ransackerで検索する
- 第7章 4つの認可
- 第8章 設定
- 第9章 RansackのためのArel入門
- 第10章 Tips詰め合わせ
- 付録
はじめに
登場人物の紹介
👦🏻 ぼく太くん。新米プラグラマー。
🐱 猫先輩。プログラミング歴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_for
はform_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_cont
のcont
のことだよ。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
などが利用できるよ。
🐱 all
のOR
版だね。
024 and - ANDで繋ぐ(カラムが複数)
🐱 and
を使うと、複数のカラムをANDで一気に検索できるよ。first_name
とlast_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_name
とlast_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>>
🐱 ここから以下の内容が読み取れるよ。
search
はRansack::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: "太郎"}
は、アドバンストモードのcondition
1つに対応する感じだね。だからシンプルモードでも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]>]
作ってみよう
🐱 それじゃあ実際にアドバンストモードで検索画面を作っていくよ。こんな感じで、属性のセレクトボックス・述語のセレクトボックス・値のサーチフィールドを用意して、ユーザーにそれぞれ入力してもらえるようにするよ。
🐱 ビューはこんな感じだよ。
# 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はこんな感じになるよ。
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セット用意されるよ。
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のセレクトボックスも用意するね。画面はこんな感じになるよ。
🐱 ビューはこんな感じだよ。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はこんな感じになるよ。
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] %>
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
🐱 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
🐱 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をセレクトボックスで指定できるような画面を作成するよ。
🐱 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セット用意するよ。
🐱 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>
🐱 これだけでソート機能の完成だよ。
カスタムのソートのリンクを用意する
🐱 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
デフォルトのソートを指定する
# users_controller.rb def index @search = User.ransack(params[:q]) # 初期状態ではsortは存在しないので、sortを作る。 @search.sorts = 'id asc' if @search.sorts.empty? @users = @search.result end
複数指定する
# 配列で複数指定可能 @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
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 - ソートのリンク
# 基本 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_at
がDATE(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しか選べなくなっているよ。
)
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で無料で読めるよ。
猫Rails 1: Ransackで簡単に検索フォームを作る73のレシピ
- 作者: 木下猫
- 発売日: 2017/05/25
- メディア: Kindle版
- この商品を含むブログを見る