Kaminariの使い方 まとめ
- 導入
- scopeメソッド
- page: nページ目のレコードを取得
- limit_value: 取得するレコード数
- total_pages: 総ページ数
- current_page: 現在のページ番号
- next_page: 次のページ番号
- prev_page: 前のページ番号
- first_page?: 1ページ目なら、true
- last_page?: 最終ページなら、true
- out_of_range?: ページが範囲外なら、true
- per: ページ毎のレコード数を設定
- padding: 最初のn件は取得しない
- total_count: 全体のレコード数
- without_count: COUNTクエリを発行しない
- except: page/per取り消し
- ビューヘルパーメソッド
- paginate: ページネーションのテンプレートをrender
- link_to_next_page: 次ページへのリンクをrender
- link_to_previous_page: 前ページへのリンクをrender
- link_to_prev_page: alias
- page_entries_info: ページ情報をrender
- rel_next_prev_link_tags:
- path_to_next_page: 次ページのpath
- path_to_prev_page: 前ページのpath
- next_page_url: 次ページのURL
- prev_page_url: 前ページのURL
- ヘルパーメソッドをコントローラーで使う
- 設定
- I18n
- テンプレートを変更する
- 配列をページネートする
- テーマ
- コンポーネント
- その他メモ
- ざっくりコードリーディング
- lib/
- kaminari-core/
- コア機能
- kaminari-core/lib/kaminari/
- kaminari-core/lib/kaminari/models/array_extension.rb
- kaminari-core/lib/kaminari/models/configuration_methods.rb
- kaminari-core/lib/kaminari/models/page_scope_methods.rb
- kaminari-core/lib/kaminari/helpers/helper_methods.rb
- kaminari-core/lib/kaminari/helpers/tags.rb
- kaminari-core/lib/kaminari/helpers/paginator.rb
- パーシャル
- kaminari-core/app/views/kaminari/
- kaminari-core/app/views/kaminari/_first_page.html.erb
- kaminari-core/app/views/kaminari/_last_page.html.erb
- kaminari-core/app/views/kaminari/_next_page.html.erb
- kaminari-core/app/views/kaminari/_prev_page.html.erb
- kaminari-core/app/views/kaminari/_page.html.erb
- kaminari-core/app/views/kaminari/_gap.html.erb
- kaminari-core/app/views/kaminari/_paginator.html.erb
- kaminari-core/config/locales/kaminari.yml
- ジェネレータ
- コア機能
- kaminari-actionview/
- kaminari-activerecord/
- 参考URL
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
- 正確な情報はドキュメントを参照してください。ドキュメントのできが良すぎてほとんどコピペみたいになってしまったので、ドキュメントを見たほうが良いです。 -> https://github.com/kaminari/kaminari
導入
インストール
# Gemfile gem 'kaminari'
$ bundle install
使ってみる
コントローラー
- ページ番号は
params[:page]
に格納される。これをpage
メソッドに食わせる
# app/controllers/items_controller.rb def index @items = Item.page(params[:page]) end
ビュー
paginate
メソッドでページネーションのテンプレートをrenderする
# app/views/items/index.html.erb <%= paginate @items %>
scopeメソッド
page: nページ目のレコードを取得
- pageは1から始まるので注意。
page(0)
はpage(1)
と同じ結果になる - デフォルトでは1ページ25レコード
- 内部的にはActiveRecordのlimitとoffsetを利用している
User.page(1) # 1ページ目の分のレコード(LIMIT 25 OFFSET 0) User.page(2) # 2ページ目の分のレコード(LIMIT 25 OFFSET 25)
- 実際にはコントローラーに
params[:page]
が渡ってくるので、それを食わせる
User.page(params[:page])
- pageは内部的にlimitを利用しているので、両方使うと上書きする
User.limit(10).count #=> 10 User.limit(10).page(1).count #=> 25
limit_value: 取得するレコード数
User.page(1).limit_value #=> 25 User.page(1).per(10).limit_value #=> 10
total_pages: 総ページ数
User.page(1).total_pages #=> 50 User.page(1).per(50).total_pages #=> 25
current_page: 現在のページ番号
User.page(1).current_page #=> 1
next_page: 次のページ番号
User.page(1).next_page #=> 2
prev_page: 前のページ番号
User.page(2).prev_page #=> 1
first_page?: 1ページ目なら、true
User.page(1).first_page? #=> true
last_page?: 最終ページなら、true
User.page(50).last_page? #=> true
out_of_range?: ページが範囲外なら、true
User.page(100).out_of_range? #=> true
per: ページ毎のレコード数を設定
- デフォルト: 25
User.page(7).per(50) # モデルに対しては直接使えない(page指定なしに使うことはありえないから) User.per(50) # エラー
padding: 最初のn件は取得しない
- OFFSETを使用して最初のn件は取得しない
- モデルに対しては直接使えない
User.page(1).padding(3) # SELECT `users`.* FROM `users` LIMIT 25 OFFSET 3
total_count: 全体のレコード数
User.count #=> 1000 User.page(1).count #=> 25 User.page(1).total_count #=> 1000
without_count: COUNTクエリを発行しない
- 参考: https://qiita.com/yuki24/items/aab0d8e417d6fe546688
- 一般的に、ページネーションではリンクを表示するために総レコード数を知らないといけない。そのためクエリを発行する必要がある
- しかし、リンクが"next"と"prev"だけでいいなら総レコード数を知る必要はない。この場合
without_count
を使うとCOUNTクエリを発行しない - 次のページが存在するか確かめるために
limit + 1
する(デフォルトならLIMIT 26
) - レコード数が大量にある場合に役立つ
# コントローラー User.page(3).without_count
- ビューでは
paginate
を使う代わりに、link_to_prev_page
とlink_to_next_page
を使い自分でリンクを用意する
# ビュー <%#= paginate @users %> <%= link_to_prev_page @users, 'Previous Page' %> <%= link_to_next_page @users, 'Next Page' %>
except: page/per取り消し
page
とper
は内部的にはActiveRecordのlimitとoffsetを利用している。なのでpageとperを取り消したくなったら、except(:limit, :offset)
を使えばOKexcept
はActiveRecordのメソッド
User.page(3).per(10) # SELECT `users`.* FROM `users` LIMIT 10 OFFSET 20 User.page(3).per(10).except(:limit, :offset) # SELECT `users`.* FROM `users`
ビューヘルパーメソッド
paginate: ページネーションのテンプレートをrender
使い方
# « First ‹ Prev ... 2 3 4 5 6 7 8 9 10 ... Next › Last » <%= paginate @users %>
オプション
window: リンク数(内側)
- デフォルト: 4
# ... 5 6 7 8 9 ... <%= paginate @users, window: 2 %>
outer_window: リンク数(外側)
- デフォルト: 0
# 1 2 3 ...(snip)... 18 19 20 <%= paginate @users, outer_window: 3 %>
left: リンク数(外側 - 左)
- デフォルト: 0
# 1 ...(snip)... 18 19 20 <%= paginate @users, left: 1, right: 3 %>
right: リンク数(外側 - 右)
- デフォルト: 0
# 1 ...(snip)... 18 19 20 <%= paginate @users, left: 1, right: 3 %>
param_name: パラメータ名
params[:page]
の:page- デフォルト: :page
# コントローラーでは`params[:page]`の代わりに`params[:pagina]`を使う <%= paginate @users, param_name: :pagina %>
params: パラメータ操作
# 上書き <%= paginate @users, params: {controller: 'foo', action: 'bar'} %> # merge # コントローラーでは`params[:hoge] #=> "piyo"` <%= paginate @users, params: {hoge: "piyo"} %>
remote: リンクがAjaxになる
- 全てのリンクに
data-remote="true"
を追加
# これだけでリンクがAjaxになる <%= paginate @users, remote: true %>
views_prefix: ViewのDirectory
- デフォルト:
kaminari/
# app/views/templates/kaminariのpartialを探す <%= paginate @users, views_prefix: 'templates' %>
theme: テーマ指定
<%= paginate @users, theme: 'my_custom_theme' %>
link_to_next_page: 次ページへのリンクをrender
# 基本 <%= link_to_next_page @items, 'Next Page' %> # Ajax <%= link_to_previous_page @items, 'Previous Page', remote: true %> # ブロックで、1ページ目の処理を指定できる <%= link_to_previous_page @users, 'Previous Page' do %> <span>At the Beginning</span> <% end %>
link_to_previous_page: 前ページへのリンクをrender
link_to_prev_page: alias
page_entries_info: ページ情報をrender
使い方
# "Displaying posts 6 - 10 of 26 in total"のような表示 <%= page_entries_info @posts %>
オプション
entry_name: 表示名を変更
# "Displaying items 6 - 10 of 26 in total"のような表示 <%= page_entries_info @posts, entry_name: 'item' %>
rel_next_prev_link_tags:
# <link rel="next" href="/users?page=5"> # <link rel="prev" href="/users?page=3"> <%= rel_next_prev_link_tags @users %>
- 実際にはタグ内で使う
<head> <title>My Website</title> # <link rel="next" href="/users?page=5"> # <link rel="prev" href="/users?page=3"> <%= yield :head %> </head> <% content_for :head do %> <%= rel_next_prev_link_tags @items %> <% end %>
path_to_next_page: 次ページのpath
# /users?page=5 <%= path_to_next_page @users %>
path_to_prev_page: 前ページのpath
# /users?page=3 <%= path_to_prev_page @users %>
next_page_url: 次ページのURL
# http://www.example.org/items?page=2 <%= next_page_url @items %>
prev_page_url: 前ページのURL
# http://www.example.org/items <%= prev_page_url @items %>
ヘルパーメソッドをコントローラーで使う
Kaminari::Helpers::UrlHelper
モジュールにヘルパーメソッドが定義されているので、コントローラーでinclude
すればOK
class UsersController < ApplicationController include Kaminari::Helpers::UrlHelper def index @users = User.page(1) path_to_next_page(@items) #=> /items?page=2 end end
設定
- kaminariのデフォルト動作の変更は、ざっくり3箇所で指定可能。同一内容の設定の場合、下が優先させる
- グローバル設定
- モデル単位の設定
- 個別の設定
グローバル設定
設定ファイルを作る
$ rails g kaminari:config create config/initializers/kaminari_config.rb
# config/initializers/kaminari_config.rb # frozen_string_literal: true Kaminari.configure do |config| # config.default_per_page = 25 # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 # config.left = 0 # config.right = 0 # config.page_method_name = :page # config.param_name = :page # config.params_on_first_page = false end
default_per_page: ページ毎のレコード数
- デフォルト: 25
max_per_page: ページ毎のレコード数の最大値
- デフォルト: nil
max_pages: ページ数の最大値
- デフォルト: nil
window: リンク数(内側)
- デフォルト: 4
outer_window: リンク数(外側)
- デフォルト: 0
left: リンク数(外側 - 左)
- デフォルト: 0
right: リンク数(外側 - 右)
- デフォルト: 0
page_method_name: pageメソッド(nページ目のレコードを取得)の名前変更
- デフォルト: :page
param_name: パラメータ名
params[:page]
の:page- デフォルト: :page
params_on_first_page: 最初のページでparamsを無視しない
- デフォルト: false
- デフォルト設定だと、最初のページでparamsを無視する。paramsを利用してフィルター機能などを実装していた場合に、それも消えてしまい問題になるので、その対策。(参考: https://qiita.com/yuki24/items/aab0d8e417d6fe546688#params_on_first_page-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%BF%BD%E5%8A%A0)
モデル単位の設定
paginates_per: ページ毎のレコード数
class Article < ActiveRecord::Base paginates_per 10 end
max_paginates_per: ページ毎のレコード数の最大値
per
スコープでこの数値以上を指定した場合、この数値が利用される- デフォルト: nil(max制限はなし)
class User < ActiveRecord::Base max_paginates_per 50 end
max_pages: ページ数の最大値
- ここで指定した以上のページがあっても、リンクを作らない
class Article < ActiveRecord::Base max_pages 100 end
個別の設定
- ページ毎のレコード数指定には
per
を使う
User.page(7).per(50)
- 見た目の変更には
patinate
のオプションを指定する
# ... 5 6 7 8 9 ... <%= paginate @users, window: 2 %> # 1 2 3 ...(snip)... 18 19 20 <%= paginate @users, outer_window: 3 %>
I18n
ラベルはI18nに対応してる
- ページネーションのラベルを変更したい場合は、ロケールファイルを変更すればOK
- 以下がデフォルトのロケールファイル(参考: https://github.com/kaminari/kaminari/blob/master/kaminari-core/config/locales/kaminari.yml)
en: views: pagination: first: "« First" last: "Last »" previous: "‹ Prev" next: "Next ›" truncate: "…" helpers: page_entries_info: entry: zero: "entries" one: "entry" other: "entries" one_page: display_entries: zero: "No %{entry_name} found" one: "Displaying <b>1</b> %{entry_name}" other: "Displaying <b>all %{count}</b> %{entry_name}" more_pages: display_entries: "Displaying %{entry_name} <b>%{first} - %{last}</b> of <b>%{total}</b> in total"
ラベルを変更する
- 変更したい場合は、ロケールファイルを用意して変更したい部分を変えればOK
# config/locales/kaminari.en.yml en: views: pagination: previous: "<-"
ラベルに日本語を使う
1. デフォルトのロケールを日本語にする
# /config/application.rb config.i18n.default_locale = :ja
2. 日本語のロケールファイルを用意する
- 参考
# config/locales/kaminari.ja.yml ja: helpers: page_entries_info: more_pages: display_entries: "<b>%{total}</b>中の%{entry_name}を表示しています <b>%{first} - %{last}</b>" one_page: display_entries: one: "<b>%{count}</b>レコード表示中です %{entry_name}" other: "<b>%{count}</b>レコード表示中です %{entry_name}" zero: "レコードが見つかりませんでした %{entry_name}" views: pagination: first: "« 最初" last: "最後 »" next: "次 ›" previous: "‹ 前" truncate: "…"
kaminari-i18n(gem)
- https://github.com/tigrish/
- 各言語に対応したロケールファイルがある
テンプレートを変更する
1. パーシャルを作成する
# defaultテーマを利用する
$ rails g kaminari:views default
- 以下の7つのパーシャルが作成される
- app/views/kaminari/_first_page.html.erb: << First
- app/views/kaminari/_last_page.html.erb: Last >>
- app/views/kaminari/_next_page.html.erb: Next >
- app/views/kaminari/_prev_page.html.erb: < Prev
- app/views/kaminari/_page.html.erb: 4(各ページへのリンク)
- app/views/kaminari/_gap.html.erb: ...(ページ間の省略)
- app/views/kaminari/_paginator.html.erb: ページネーションHTML全体。各partialはここで使われる
2. haml/slimに変換する
- erbではなくhaml/slimを扱いたい場合は
html2haml
やhtml2slim
を使って、自分で変換する。 - 昔は
-e haml
オプションでhamlを生成できたが、現在はdeprecated
3. パーシャルを変更する
- 作成されたパーシャルはkaminari内部で使用されているものと同じ。しかしこちらのほうが優先されるので、こちらを自分で変更することで、テンプレートを変更できる。
配列をページネートする
- Relationだけでなく、配列もページネート可能
@paginatable_array = Kaminari.paginate_array(my_array_object).page(params[:page]).per(10)
total_countを指定
- 実際のcountとは異なった値を返すような、配列っぽいオブジェクトに対して使う(ドキュメントではRSolr)
@paginatable_array = Kaminari.paginate_array([], total_count: 145).page(params[:page]).per(10) @paginatable_array.total_count #=> 145 @paginatable_array.count #=> 0
テーマ
- テーマを変更することで、ページネーション部分のHTMLを変更できる
- 各CSSフレームワークに対応したテーマが多いっぽい
- デフォルトのテーマは
default
使い方
# defaultテーマを利用 $ rails g kaminari:views default # bootstrap4用のテーマを利用 $ rails g kaminari:views bootstrap4
テーマ一覧
- default
- bootstrap2
- bootstrap3
- bootstrap4
- bourbon: Bourbon
- bulma
- foundation
- foundation5
- github
- materialize
- purecss
- semantic
kaminari_themes(gem)
- https://github.com/amatsuda/kaminari_themes
- テーマはこのリポジトリで管理されている
$ rails g kaminari:views bootstrap4
時にはこのリポジトリから取ってくる
復数のテーマを使う
1. カスタムテーマ用のディレクトリを用意する
# 1. defaultでテンプレート作成 % rails g kaminari:views default # 2. cd % cd app/views/kaminari # 3. カスタムテーマ用のディレクトリを作る % mkdir my_custom_theme # 4. テンプレートをすべてコピー % cp _*.html.* my_custom_theme/
2. paginateでカスタムテーマ用のディレクトリを参照する
<%= paginate @users, theme: 'my_custom_theme' %>
コンポーネント
- kaminariはコンポーネントベースで構成されている
- 主要コンポーネントは以下の3つ。これらは単独のgemだがkaminariにバンドルされてる
- kaminari-core: コアロジック
- kaminari-activerecord: Active Recordアダプタ
- kaminari-actionview: Action Viewアダプタ
- コンポーネントは他にも色々ある。コンポーネントベースな設計のおかげで、自分の環境にあったコンポーネントを選んで使うことができる。
使い方
# ActiveRecord + Rails(ActionView)の場合 # 普通にkaminariを使う場合はこうなる gem 'kaminari' # ActiveRecord + Rails(ActionView)の場合 # kaminari gemは以下の3つのgemからなるので、個別にinstallしてもOK # こんな感じでコンポーネントを選ぶことが可能 gem 'kaminari-activerecord' gem 'kaminari-actionview' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能 # Mongoid + Rails(ActionView)の場合 gem 'kaminari-mongoid' gem 'kaminari-actionview' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能 # ActiveRecord + Sinatraの場合 gem 'kaminari-activerecord' gem 'kaminari-sinatra' gem 'kaminari-core' # これは2gemと依存関係にあるので省略可能
コンポーネント一覧
ORM
kaminari-activerecord: Active Record
kaminari-mongoid: Mongoid
kaminari-mongo_mapper: MongoMapper
kaminari-data_mapper: DataMapper
フレームワーク
kaminari-actionview: Rails(Action View)
- https://github.com/kaminari/kaminari/tree/master/kaminari-actionview
- Kaminari gem内にある
kaminari-sinatra: Sinatra
kaminari-grape: Grape
その他メモ
ちゃんとorderを使う
- 内部的にはActiveRecordのlimitとoffsetを利用している。なのでちゃんとorderを使う必要がある
# bad User.page(params[:page]) # good User.order('name').page(params[:page])
ユーザーフレンドリー + ページキャッシュ
/users?page=33
を/users/page/33
でアクセスできるようにすると良いらしい- ユーザーフレンドリーになる
- ページキャッシュが効く
# routes.rb # concern使わない場合 resources :users do get 'page/:page', action: :index, on: :collection end # concernを使う場合 concern :paginatable do get '(page/:page)', action: :index, on: :collection, as: '' end resources :users, concerns: :paginatable
1.0.0で大きく変わってるので注意する
- 変更点
- コンポーネント化
- generatorのhaml/slimテンプレート作成機能がDeprecated(
-e haml
、-e slim
) - without_count追加
- 他にも色々
管理画面のテンプレートを用意する
- 管理画面等のテンプレートが必要になった場合には、ジェネレータで
--views-prefix
オプションを使う
$ rails g kaminari:views default --views-prefix admin
create app/views/admin/kaminari/_next_page.html.erb
create app/views/admin/kaminari/_last_page.html.erb
create app/views/admin/kaminari/_first_page.html.erb
create app/views/admin/kaminari/_page.html.erb
create app/views/admin/kaminari/_paginator.html.erb
create app/views/admin/kaminari/_prev_page.html.erb
create app/views/admin/kaminari/_gap.html.erb
サポート
- Ruby 2.0.0, 2.1.x, 2.2.x, 2.3.x, 2.4.x, 2.5.x, 2.6
- Rails 4.1, 4.2, 5.0, 5.1, 5.2
- Sinatra 1.4
- Haml 3+
- Mongoid 3+
- MongoMapper 0.9+
- DataMapper 1.1.0+
ざっくりコードリーディング
- v1.1.1(現在の最新)が対象
- どこに何があるか?程度
lib/
- lib/配下はkaminari.rbとバージョンファイルしかない。実際のコードは各コンポーネントにある
lib/kaminari.rb
- 3つの主要コンポーネントをrequire
- これで
gem "kaminari"
で一通りの機能が使えるようになる
require 'kaminari/core' require 'kaminari/actionview' require 'kaminari/activerecord'
kaminari-core/
- kaminariのコアロジック
- コレ自体がgem
コア機能
kaminari-core/lib/kaminari/
- コア機能置き場
kaminari-core/lib/kaminari/models/array_extension.rb
Kaminari.paginate_array
のロジック
kaminari-core/lib/kaminari/models/configuration_methods.rb
paginates_per
等のモデルの設定用のクラスメソッド群
kaminari-core/lib/kaminari/models/page_scope_methods.rb
per
等のscopeメソッド群
kaminari-core/lib/kaminari/helpers/helper_methods.rb
paginate
等のヘルパーメソッド群
kaminari-core/lib/kaminari/helpers/tags.rb
- kaminariのページネーションで使われるHTMLを表現するTagクラス
- TagのサブクラスとしてPrevPage等があり、それらは
app/views/kaminari/_prev_link.html.erb
等に対応する
kaminari-core/lib/kaminari/helpers/paginator.rb
- TagのサブクラスとしてPaginatorがあり、
app/views/kaminari/_paginator.html.erb
等に対応する(?)
パーシャル
kaminari-core/app/views/kaminari/
- パーシャル置き場
$ rails g kaminari:views default
で作成するパーシャルでもある- erb以外にもhaml/slimもある。Deprecatedなだけでまだ使えるっぽい
kaminari-core/app/views/kaminari/_first_page.html.erb
- "<< First"となる、1ページ目へのリンクのパーシャル
t('views.pagination.first')
でロケールファイルを利用
<span class="first"> <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %> </span>
kaminari-core/app/views/kaminari/_last_page.html.erb
- "Last >>"
kaminari-core/app/views/kaminari/_next_page.html.erb
- "Next >"
kaminari-core/app/views/kaminari/_prev_page.html.erb
- "< Prev"
kaminari-core/app/views/kaminari/_page.html.erb
- "4"となる、各ページへのリンクのパーシャル
kaminari-core/app/views/kaminari/_gap.html.erb
- "..."となる、ページ間の省略を表すパーシャル
kaminari-core/app/views/kaminari/_paginator.html.erb
paginate
ヘルパーでrenderするやつ。ページネーションHTML全体。_first_page.html.erb
等のパーシャルはここで利用される- kaminari-core/lib/kaminari/helpers/paginator.rbのKaminari::Helpers::Paginatorインスタンスを利用して、renderしてる。
first_page_tag
などもpaginator.rbで定義
<%= paginator.render do -%> <nav class="pagination" role="navigation" aria-label="pager"> <%= first_page_tag unless current_page.first? %> <%= prev_page_tag unless current_page.first? %> <% each_page do |page| -%> <% if page.display_tag? -%> <%= page_tag page %> <% elsif !page.was_truncated? -%> <%= gap_tag %> <% end -%> <% end -%> <% unless current_page.out_of_range? %> <%= next_page_tag unless current_page.last? %> <%= last_page_tag unless current_page.last? %> <% end %> </nav> <% end -%>
kaminari-core/config/locales/kaminari.yml
- デフォルトのロケールファイル
- パーシャルはここを参照してる
ジェネレータ
kaminari-core/lib/generators/kaminari/config_generator.rb
$ rails g kaminari:config
に対応
kaminari-core/lib/generators/kaminari/views_generator.rb
$ rails g kaminari:views
に対応
kaminari-actionview/
- ActionViewモジュール
- ActionView::Baseにヘルパー機能をincludeして、ビューでヘルパー使えるようにしてる
- 他にもちょい
kaminari-activerecord/
- ActiveRecordモジュール
- ActiveRecord::Baseにscope/config機能をincludeしてる
参考URL
Draperの使い方 まとめ
- 感想とか
- 基本的な使い方
- その他メモ
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
感想とか
感想
- ヘルパーをオブジェクト指向的に書けるようになる
- 個人的には類似gemのActiveDecoratorのがシンプルで好みかも。でもDraperも十分良さそう
pros
- 高機能
- 関連先もデコれる
- Decorator gemでは一番利用者が多いっぽい
cons
- 毎回明示的にデコらないといけない(prosと考えることもできる)
- どこでもデコれるのは自由度がありすぎてカオスになりそう ->
decorates_assigned
を使えば回避できるかも
基本的な使い方
導入
1. インストール
# Gemfile gem 'draper'
$ budle install
2. セットアップ
# app/decorators/application_decorator.rb を作成 # ApplicationDecoratorは全てのDecoratorの親クラスになる $ rails generate draper:install
3. デコレータを作成して、使用する
# app/decorators/article_decorator.rb を作成
$ rails generate decorator Article
# app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator # Articleにのメソッドが使えるようになる。published?がそれ delegate_all # ビューで`@article.publication_status`のように、@articleのメソッドとして利用できる def publication_status # published?はArticleのメソッドだけど、ArticleDecoratorでもそのまま使える if published? "Published at #{published_at.strftime("%A, %B %e")}" else "Unpublished" end end end
# ビュー <%= @article.publication_status %>
ヘルパー/ビュー/モデルにロジックを置く場合との比較
ヘルパーにロジックを置く場合
- 手続き的で、オブジェクト指向的でない
- 名前空間がグローバル(ファイルは分けらる)
- 名前衝突の可能性
- どこに定義されているかわからない
# app/helpers/articles_helper.rb def publication_status(article) if article.published? "Published at #{article.published_at.strftime('%A, %B %e')}" else "Unpublished" end end # ビュー <%= publication_status(@article) %>
ビューにロジックを置く場合
- 手続き的で、オブジェクト指向的でない
- メンテナンスしにくい
- 読みにくい
<% if @article.published? %> <%= "Published at #{@article.published_at.strftime('%A, %B %e')}" %> <% else %> Unpublished <% end %>
モデルに置く場合
- モデルにはドメインロジックを置きたい。プレゼンテーションロジックがモデルにあるのは微妙
- HTMLを描画するための機能(helper等)が利用できない
- fat model
# app/decorators/article_decorator.rb class Article < ApplicationRecord def publication_status if published? "Published at #{published_at.strftime("%A, %B %e")}" else "Unpublished" end end end
# ビュー <%= @article.publication_status %>
デコレータにロジックを置く場合
- オブジェクト指向的
- プレゼンテーションロジックをモデルから分けられる
# app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status if published? "Published at #{published_at.strftime("%A, %B %e")}" else "Unpublished" end end end
# ビュー <%= @article.publication_status %>
ヘルパーメソッドを使う*2
hメソッド: ヘルパーにアクセス
h
メソッドを通して、ヘルパーにアクセスできる- Railsのヘルパー、自作ヘルパー両方にアクセスできる
helpers
でもok
class ArticleDecorator < Draper::Decorator def emphatic # helpers.content_tag(:strong, "Awesome") でもok h.content_tag(:strong, "Awesome") end end
include Draper::LazyHelpers: ヘルパーを直接include
- hを介さず、ヘルパーを直接include
- でも大量のヘルパーが定義されちゃって、デコレーター/モデル/ヘルパーのメソッドがごっちゃになるから、いまいちっぽい
class ArticleDecorator < Draper::Decorator include Draper::LazyHelpers end
モデルのメソッドを使う*3
object: モデルインスタンスにアクセス
object.published_at.strftime("%A, %B %e") model.published_at.strftime("%A, %B %e") # modelはobjectのalias
delegate_all: 全てのメソッドを委譲
- 内部的には
method_missing
を使い委譲している - メソッド探索の順番: デコレータ -> 親デコレータ -> モデル
class ArticleDecorator < Draper::Decorator delegate_all end
delegate: 指定のメソッドを委譲
- deleageメソッドはActive Supportのdelegateとほぼ同じ。ただし
to: :object
を省略できる。
class ArticleDecorator < Draper::Decorator # 指定のメソッドのみ委譲する(toオプションを指定しないとデフォルトでobjectに委譲する) # titleで、object.titleにアクセスできるようになる delegate :title, :body # 指定のメソッドを指定したオブジェクトに委譲する delegate :name, :title, to: :author, prefix: true end
オブジェクトをデコる*3
基本
# app/controllers/articles_controller.rb def show # articleをデコる @article = Article.find(params[:id]).decorate end
デコる方法は3つ
- 一番オススメ
- デコレータをモデル名から推測してくれる(そのためデコレータ名がモデル名から推測できないといけない)
@article = Article.find(params[:id]).decorate
- こちらならデコレータの名前が違う場合でもOK
# 名前同じ場合 @article = ArticleDecorator.new(Article.find(params[:id])) @article = ArticleDecorator.decorate(Article.find(params[:id])) # newと同等 # 名前違ってもOK @widget = ProductDecorator.new(Widget.first) @widget = ProductDecorator.decorate(Widget.first)
.decorates_finders
を使うと、デコレータがfind系(find/all/first/...)のメソッドが利用できるようになる。これでfindしつつデコることが可能- でもいまいちな気がする。Draperのデコレータはビューで使うことを想定している。DBアクセスはモデルがやるべきで、デコレータの仕事じゃない
# app/decorators/articl_decorator.rb class ArticleDecorator < Draper::Decorator decorates_finders end # コントローラー @article = ArticleDecorator.find(params[:id])
コレクションの各要素をデコる*2
- コレクションを操作すると、各要素は一気にデコられる
# オススメ # Relationの場合、そのままチェーンできる @articles = Article.recent.decorate # こっちでもできる # 配列の場合、`ArticleDecorator.decorate_collection`に食わせる # Rails3だとallは配列を返すので、こっちを使う @articles = ArticleDecorator.decorate_collection(Article.all)
コレクション自体をデコる
- モデルのクラスメソッドを生やすイメージ
- Draper::CollectionDecoratorを継承
- 使わなそう -> patinationのメソッド生やすのに使える
# app/decorators/articles_decorator.rb # ArticleDecoratorではない。複数形になるので注意。 class ArticlesDecorator < Draper::CollectionDecorator def page_number 42 end end @articles = Article.all.decorate # ArticlesDecoratorオブジェクト(ActiveRecordのRelation相当) @articles.page_number # => 42
関連先をデコる*2
メソッド
decorates_association: 関連先をデコる
# app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator # ArticleDecoratorがArticleをデコレートした時に、自動でAuthorDecoratorがAuthorをデコレートする decorates_association :author end # app/decorators/author_decorator.rb class AuthorDecorator < Draper::Decorator def hoge model.name + "hogehoge" end end # Articleしかデコレートしていないのに、authorまでデコレートできている article = Article.first.decorate article.author.hoge
decorates_associations: 関連先をデコる(複数)
# app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator decorates_associations :author, :comments end
オプション
with: 関連先のデコレーターを指定する
class ArticleDecorator < Draper::Decorator # AuthorDecoratorではなく、FancyPersonDecoratorでデコレート decorates_association :author, with: FancyPersonDecorator end
scope: 関連先をscopeで絞り込む
- decorate対象の絞り込ではなく、レコード自体の絞り込み
class ArticleDecorator < Draper::Decorator # `article.comments`時に、`recent`で絞り込む decorates_association :comments, scope: :recent end
context: 関連先にcontextを渡す
# 関連先にcontextを渡す class ArticleDecorator < Draper::Decorator decorates_association :author, context: {foo: "bar"} end # ラムダで上書きすることも可能 # parent_contextはhashだよ class ArticleDecorator < Draper::Decorator decorates_association :author, context: ->(parent_context){ parent_context.merge(foo: "bar") } end
POROをデコる
- ActiveRecord::BaseにはDraper::Decoratableモジュールがincludeされている。これにより
Article.first.decorate
のような処理が可能になる - POROにもDraper::Decoratableモジュールをincludeすれば同じことが可能になる
class Cat include Draper::Decoratable end class CatDecorator < ApplicationDecorator def nyaa "nyaa" end end Cat.new.decorate.nyaa # => "nyaa"
- ActiveModel::Modelとかのformで使うようなオブジェクトをデコレートする時に便利そう
class ArticleForm include ActiveModel::Model include Draper::Decoratable attr_accessor :field1, ... end
自動でデコる
- デコレーターはモデルのように振る舞えるので、actionの開始時にデコレートして、ずっとデコレータを使いたくなる。でもコレは非推奨。ビューで使うように設計されているので、ビュー以外で使うべきでない。そのためコントローラーではモデルを使い、render直前にデコって、ビューではデコレータを使う
decorates_assigned
を使うとこれを自動化できる。コントローラーでは@article
をモデルとして扱い、ビューではarticle
をデコレーターとして扱える。decorates_assigned
はGoRailsでもおすすめしてた。draperは結構自由にデコレートできてしまいカオスになりそうなので、コレを使って自動化しておくとよさそう(参考: https://gorails.com/episodes/decorators-with-draper)
# app/controllers/articles_controller.rb class ArticlesController < ApplicationController # `decorates_assigned`を使うと、 # コントローラーでは`@article`をモデルとして扱い、 # ビューでは`article`をデコレータとして扱える。 # # 具体的には以下の2つをしてくれる # - @articleインスタンス変数をデコってくれる # - helper_methodを使い、ビューで`article`でアクセスできるようにする decorates_assigned :article def show @article = Article.find(params[:id]) end end
# app/views/articles/show.html.erb <%= article.decorated_title %>
- イメージとしてはこんな感じのことをしてくれてる
def article @decorated_article ||= @article.decorate end helper_method :article
- indexでも使う場合は複数形も必要
decorates_assigned :article, :articles
ページネーションと一緒に使う
問題点
- kaminari等のページネーションgemと一緒に使うには一工夫必要
- ページネーションgemはRelationにcurrent_page等のメソッドを追加する。そのままデコレートしてしまうとcurrent_page等のメソッドにアクセスできなくなってしまう。
cats = Cat.all # Cat::ActiveRecord_Relationインスタンス cats = cats.page # Cat::ActiveRecord_Relationインスタンス cats.current_page # => 1 cats = cats.decorate # CatsDecoratorインスタンス cats.current_page # NoMethodErrorになってしまう
解決方法1: Draper::CollectionDecorator
でdelegate
- 解決方法は色々あるっぽいが、デコレータコレクションの親クラスである
Draper::CollectionDecorator
でdelegate
してしまうのが簡単そう。 - 参考
# config/initializers/draper.rb Draper::CollectionDecorator.delegate :current_page, :total_pages, :limit_value, :total_count
解決方法2: Draper::CollectionDecorator
のサブクラスをコレクションにする
- サブクラスを噛ませるだけで、やってることは1とほとんど同じっぽい
- 参考: https://elsapet.wordpress.com/2014/10/01/draper-collectiondecorator-and-pagination-with-kaminari/
# Draper::CollectionDecoratorのサブクラスを作って、delegate class PaginatingDecorator < Draper::CollectionDecorator delegate :current_page, :total_pages, :limit_value, :total_count end # コレクションがPaginatingDecoratorになるようにする class ApplicationDecorator < Draper::Decorator def self.collection_decorator_class PaginatingDecorator end end # 各DecoratorはApplicationDecoratorを継承する class AnimalDecorator < ApplicationDecorator end
その他メモ
Decorator/Presenter/ViewModel/Exibitの違い
- Decorator/Presenter/ViewModel/Exibitの違いがわからん
- いくつか記事を見た感じ、どうも文脈/人によって同じ言葉でも意味が変わるっぽい
- draperの文脈では全て同じもの(ビューヘルパーをオブジェクト指向的に扱うための、モデルのデコレータ)を指すことが多い気がする
- draperのDecoratorはGoFのDecoratorパターンに由来すると思われるが、ビュー以外での使用は想定していない(はず)。あんまり名前にこだわりすぎないほうがいいかもしれない
参考サイトのまとめ
Decorator と Presenter を使い分けて、 Rails を ViewModel ですっきりさせよう
- チーム内で認識を統一して、用語を使い分けてる
- 良さそう。でも難しい。
- 用法
- Decorator: 単一のモデルクラスに対応する ViewModel
- Presenter: 複数のモデルクラスにまたがる ViewModel、永続化されたモデルと一致しない ViewModel
- ViewModel: Decorator 、Presenter の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。
Exhibit vs Presenter
- 全体的に難しい。あんまり理解できてない
- 用法
- Decorator: Presenter、Exibitの上位概念。GofのDecoratorと同じで、オブジェクトに機能/振る舞いを追加するもの。サンプルではSimpleDelegatorやDecoratorを使い実装
- Presenter: Decoratorの一種。モデルをデコレートするプレゼンテーションロジック置き場?(自信なし)
- Exibit: Decoratorの一種。モデルをビューに繋げる。Exibit自体はビューの機能を持たず、contextを持ち、contextにrenderさせる
- Presenter + Exhibit: PresenterとExhibitは排他的な関係ではない。同時に実現可能。draperはこれ?
model の decorator の話
- 用法
- Presenter: 一つのView につき一つの Decorator
- ActiveDecorator(gem): 一つの model につき一つの Decorator(たぶんdraperもコレに該当)
- Exhibit: View x Model の組み合わせにつき一つの Decorator
デコレータのクラス*2
Draper::Decorator: モデルインスタンスのデコレータ
- article.decorateの戻り値は、このクラスのサブクラス
- 基本コレをつかう
# app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator # Articleにのメソッドが使えるようになる。published?がそれ delegate_all # ビューで`@article.publication_status`のように、@articleのメソッドとして利用できる def publication_status # published?はArticleのメソッドだけど、ArticleDecoratorでもそのまま使える if published? "Published at #{published_at}" else "Unpublished" end end def published_at object.published_at.strftime("%A, %B %e") end end
# ビュー <%= @article.publication_status %>
Draper::CollectionDecorator: コレクションのデコレータ
- articles.decorateの戻り値は、このクラスのサブクラス
- 使わなそう
# app/decorators/articles_decorator.rb class ArticlesDecorator < Draper::CollectionDecorator def page_number 42 end end @articles = Article.all.delegate @articles = ArticlesDecorator.new(Article.all) @articles = ArticlesDecorator.decorate(Article.all)
contextで追加データを渡す
# デコレータ内で、contextメソッドを通して利用できる Article.first.decorate(context: {role: :admin})
デコレータでHTMLをレンダリング
- 参考: https://ruby-rails.hatenadiary.com/entry/20150415/1429031791
- 短い場合は、
h.content_tag
とかを使って組み立てる
# デコレータ def emphatic h.content_tag(:strong, "Awesome") # => <strong>Awesome</strong> end
- 長い場合は、
h.render
で部分テンプレートを使って組み立てる
# デコレータ def sub_view h.render 'sub_view', title: model.title # => articles/sub_view.html.erb を表示する end
デコレータはモデルのように振る舞う
- form_forなどで上手く機能するように、特別にカスタマイズされてる
- ハマりそうなので注意
- 参考: https://bytes.babbel.com/en/articles/2014-05-12-rails-decorators.html
@person = Person.first @decorated_person = @person.decorate @decorated_person.is_a?(Person) # true @person == @decorated_person # true
モデルメソッドをオーバーライドする
- 便利だけどわかりにくくなりそう
# デコレータ def created_at article.created_at.strftime("%m/%d/%Y - %H:%M") end # ビュー @article.created_at
ApplicationDecorator
- ApplicationRecordに合わせて、Draper::DecoratorではなくApplicationDecoratorを継承すると吉
# app/decorators/application_decorator.rb class ApplicationDecorator < Draper::Decorator # 全てのデコレータで共有する処理はここに置く end # app/decorators/article_decorator.rb # Draper::Decoratorではなく、ApplicationDecoratorを継承する class ArticleDecorator < ApplicationDecorator end
ApplicationControllerの名前変更
- ベースとなるコントローラーはApplicationControllerで、draperもこれを仮定している
- 変更したい場合は以下の通り
Draper.configure do |config| config.default_controller = BaseController end
Active Jobとの統合
- デコレートされたオブジェクトをJobとしてシリアライズできるが、デシリアライズ時にはデコレートは解除されている。モデルオブジェクトのGlobal IDを使ってシリアライズをするため。
テスト
- RSpec, MiniTest::Rails, Test::Unitの3つに対応
- decoratorをgenerateした場合、テストも追加してくれる
RSpec
- デフォルトのパスはspec/decorators。パスを変えるなら、
type: :decorator
が必要
RSpecのコントローラーテスト
# デコられているかテストできる述語マッチャ(predicate matchers) assigns(:article).should be_decorated # デコレーターを指定できる assigns(:article).should be_decorated_with ArticleDecorator
ApplicationController以外を使う場合はコレが必要
# spec_helper.rb config.before(:each, type: :decorator) do |example| Draper::ViewContext.controller = ExampleEngine::CustomRootController.new end
isolated test
- Draperはヘルパーメソッドにアクセスするために、view contextを必要とする
- デフォルトではApplicationControllerのview contextを使う
- この依存を取り除いて、テストをスピードアップしたい場合は以下の設定をする。ただしデコレーターはヘルパーにアクセスできなくなる
# spec_helper # 依存を取り除く Draper::ViewContext.test_strategy :fast # 個別にHelperをinclude Draper::ViewContext.test_strategy :fast do include ApplicationHelper end
1.0へのアップデート
- 2013に1.0へのアップデートがあって、かなり変更が入ったらしい
- 主要なものだけ
Draper::Base -> Draper::Decorator
Draper::DecoratorEnumerableProxy -> Draper::CollectionDecorator
Draper::ModelSupport -> Draper::Decoratable
自動的に委譲 -> delegate_allで明示的に委譲
委譲の制御allows/denies/denies_allは削除
delegate_allでクラスメソッドも委譲する
デフォでfind系メソッドは追加されない
その他の使いそうなメソッド
l: デコレータでlocalizeメソッド
- よく使うので
helpers.localize
はl
で使える localize
でもok
decorator.decorated_with?: 指定クラスでデコレートされていればtrue
Cat.first.decorate.decorated_with?(CatDecorator) # => true
decorator.decorated?: オブジェクトがデコレートされていればtrue
Article.first.decorate.decorated? # => true Article.first.decorated? # => false
Pumaの使い方 まとめ
- 感想
- スレッドベース
- railsへの導入
- 設定
- 参考URL
- 設定ファイルの読み込み
- 設定項目
- bind: バインド
- port: バインド(portとhost)
- ssl_bind: バインド(SSL)
- workers: ワーカー数
- threads: スレッド数のmin・max
- environment: 環境
- demonize: デーモン化
- pidfile: pidファイル置き場
- stdout_redirect: 標準出力/標準エラーを出力するファイル
- preload_app!: プリロード
- before_fork{}: 各ワーカーのフォーク前の処理
- on_worker_boot{}: 各ワーカーのboot前の処理
- prune_bundler: phased_restart時にbundlerのコンテキストを新しいものに切り替えてくれる
- plugin: プラグイン読み込み
- activate_control_app: コントロールサーバのURL
- state_path: stateファイルのパス
- directory: 起動ディレクトリ
- on_worker_shutdown: 各ワーカーのshutdown前の処理
- tag: プロセスリストでの追加情報
- worker_timeout: 全てのワーカーがマスタープロセスにチェックインする、タイムアウト時間
- worker_boot_timeout: ワーカーのブートのタイムアウト時間
- lowlevel_error_handler{}: アプリ外の例外のエラーハンドリング
- app: Rackアプリ
- rackup: アプリ起動ファイルのパス
- quiet: リクエストロギングをdisable
- log_requests: リクエストロギングをenable
- restart_command: pumaの再起動に使用するコマンド
- load: 追加のconfigファイルをロード
- on_restart{}: リスタート前の処理
- persistent_timeout: persistent connectionsのタイムアウト
- first_data_timeout: 受信無しでtcpソケットを開き続ける際のタイムアウト
- tcp_mode!: pumaをTCPモードで起動
- queue_requests: リクエストをキューする
- shutdown_debug: shutdown時にバックトレースを出力
- debug: デバグ情報を出力
- early_hints: Early Hintsのサポートを有効にする
- 他にも色々
- プロセス管理
- シグナル
- pumaコマンド
- オプション
- -C, --config PATH: 設定ファイル読み込み(loadに対応)
- -b, --bind URI: バインド(bindに対応)
- -t, --threads INT: スレッド数(threadsに対応)
- -w, --workers COUNT: workers数(workersに対応)
- -d, --daemon: デーモン化(daemonizeとquietに対応)
- -e, --environment ENVIRONMENT: 環境(environmentに対応)
- -p, --port PORT: bind(portに対応)
- -q, --quiet: リクエストロギングをdisable(quietに対応)
- -v, --log-requests: リクエストロギングをenable(log_requestsに対応)
- -R, --restart-cmd CMD: pumaの再起動に使用するコマンド(restart_commandに対応)
- -S, --state PATH:stateファイルのパス(state_pathに対応)
- --tcp-mode: TCPモード(tcp_mode!に対応)
- --early-hints: Early Hintsのサポートを有効にする(early_hintsに対応)
- --debug: デバグ情報を出力(debugに対応)
- --dir DIR: directory: 起動時ディレクトリ(directoryに対応)
- --pidfile PATH: PIDファイルパス(pidfileに対応)
- --preload: プリロード(preload_app!に対応)
- --prune-bundler: bundler切り替え(prune_bundlerに対応)
- --tag NAME: プロセスリストでの追加情報(tagに対応)
- --redirect-stdout FILE: 標準出力のリダイレクト先ファイル(stdout_redirectに対応)
- --redirect-stderr FILE, 標準エラー出力のリダイレクト先ファイル(stdout_redirectに対応)
- --[no-]redirect-append: 追記モード(stdout_redirectに対応)
- --control-url URL: コントロールサーバのurl(activate_control_appに対応)
- --control URL: --control-urlと同じ(activate_control_appに対応)
- --control-token TOKEN: コントロールサーバの認証用トークン(activate_control_appに対応)
- -I, --include PATH: $LOAD_PATHに追加
- -V, --version: バージョン
- -h --help: ヘルプ
- オプション
- pumactlコマンド
- railsコマンド
- 結局どうすれば?
- その他メモ
- puma関係のgem
- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
感想
- 新規にRailsアプリ作るならpuma使えば良さそう。でも、unicornやpassengerから乗り換えるほどではないかも(ケースバイケースだがパフォーマンスの顕著な差はないっぽい)
- Heroku使う場合はメモリ使用量が少なくて良さそう。メモリ500MBプランだとunicornで2worker動かすの辛かった記憶
スレッドベース
参考
- https://blog.willnet.in/entry/2015/02/24/155006
- http://railscasts.com/episodes/365-thread-safety?view=asciicast
- https://blog.yuuk.io/entry/2015-webserver-architecture
- http://d.hatena.ne.jp/naoya/20071010/1192040413
unicornはプロセスベース
- workerプロセスが2つあったら、2つのリクエストを同時に処理できる
- メリット
- シンプル(理解しやすい + コードが綺麗になりやすい)
- スレッドの知識がなくても安心
- スレッドセーフなコードを意識しなくても良い(普通にRailsアプリを書いていれば問題なさそうだけど、スレッドの知識が浅いので不安)
pumaはスレッドベース
- スレッドが2つあったら、2つのリクエストを同時に処理できる
- 実際には本番環境ではマルチプロセス + マルチスレッドで動かす(Clustered mode)。workerプロセスが2つ + スレッドが2つだったら、4つのリクエストを同時に処理できる
- ただし実際には、MRIではGILがあるため1プロセスで1スレッドしか実行されない
- メリット
- (MRIだとしても)IO時に別スレッドに処理させることができる
- スロークライアントの影響を受けにくい
- メモリ使用量が少ない(参考: http://puma.io/)
MRIのスレッド
- スレッドは複数持てるが、同時に実行できるスレッドは1つ。GIL(Global Interpreter Lock)のため。
- ただし、Bocking IO(ファイルIO、ネットワークIO等)になった際に、別のスレッドに切り替えて処理を進める。MRIでもスレッドベースの恩恵は部分的にある。
- IOはだいたい総時間の10~25%程度らしい(参考: https://techracho.bpsinc.jp/hachi8833/2017_11_13/47696)
Jruby・Rubiniusのスレッド
- スレッドをフルに使える
- pumaにはJrubyやWindows特有の成約がいくつかある(別項目)
- MRI以外使うことはないかなー
スロークライアント
- 回線の遅いクライアント(3Gのモバイル端末など)
- Pumaはスレッドベースなので、IOの際に(MRIだとしても)別のスレッドに処理をさせることができる。なのでネットワークIOが長いスロークライアントには都合がよい
railsへの導入
# Gemfile gem 'puma'
$ bundle install
# 起動 # $ rails s Puma $ rails s
設定
参考URL
- https://github.com/puma/puma/blob/master/lib/puma/dsl.rb
- https://github.com/puma/puma/blob/master/examples/config.rb
設定ファイルの読み込み
- 3つの起動コマンド(
rails s
、pumactl start
、puma
)は、以下の設定ファイルを自動で読み込む- 環境指定がない場合: config/puma.rb
- 環境指定がある場合: config/puma/<environment_name>.rb
pumactl -F config/puma.rb start
のように、オプションで指定することも可能
設定項目
bind: バインド
- URI指定しかないので、シンプル
- デフォルト: "tcp://0.0.0.0:9292"
# TCPソケットを使う場合 bind 'tcp://0.0.0.0:9292' # UNIXソケットを使う場合 # TCPより若干パフォーマンスが上がる(場合がある) bind 'unix:///var/run/puma.sock' # UNIXソケットのパーミッションを変更する必要がある場合、umaskパラメーターを利用する bind 'unix:///var/run/puma.sock?umask=0111' # SSLを利用する場合 bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
port: バインド(portとhost)
- bind使えばok
# bind 'tcp://0.0.0.0:9292' と同じ port '9292', '0.0.0.0'
ssl_bind: バインド(SSL)
- bind使えばok
# bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert' と同じ ssl_bind '127.0.0.1', '9292', { key: path_to_key, cert: path_to_cert }
workers: ワーカー数
- ワーカー数を指定するとclustered modeになる
- master processからworkerをforkする
- workerプロセスはそれぞれスレッドプールを持つ
- デフォルト: 0
# ワーカー2スレッド16の場合、スレッドは合計で32になる workers 2
threads: スレッド数のmin・max
- プールで利用できるスレッドの数
- スレッド数はtrafficによって自動で増減する
- maxを大きくしすぎるとマシンリソースを食いつくしてしまう可能性があるので、注意
- ここで指定したスレッド以外にも、スロークライアントの処理等のpuma自体の内部的な処理にもスレッドが作られるので注意。なので
-t 1:1
で指定しても、実際には7スレッドくらいが作成される。 - デフォルト: 0:16
threads 16, 16
environment: 環境
- デフォルト: "development"
environment 'production' environment ENV.fetch("RAILS_ENV") { "development" } # railsで使う場合は、環境変数RAILS_ENVを使うのが良さげ
demonize: デーモン化
- デフォルト: false
- pidfile、stdout_redirectと一緒に使う
daemonize true
pidfile: pidファイル置き場
pidfile '/u/apps/lolcat/tmp/pids/puma.pid' pidfile "#{Dir.pwd}/tmp/pids/puma.pid" # Railsの場合、tmp/pids/puma.pidに置くと良さげ
stdout_redirect: 標準出力/標準エラーを出力するファイル
- 第三引数は追記モード
stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr' stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr', true
preload_app!: プリロード
- 全てのアプリコードをfork前にロードする。これによりRuby 2.0+の場合、OSのcopy-on-writeが効く。そのためメモリ使用量が下がる
- Cluster modeでしか使えない
- phased-restartとpreloadは同時には使えない
preload_app!
before_fork{}: 各ワーカーのフォーク前の処理
- preload_app!時に、DBやredisのコネクションを閉じるのに使う -> 不要?(参考: https://paulownia.hatenablog.com/entry/2018/08/13/211856)
before_fork do ActiveRecord::Base.connection_pool.disconnect! end
on_worker_boot{}: 各ワーカーのboot前の処理
- ブート前のセットアップはここで行う
- アプリ固有ではなく、puma固有の処理はここで行う
- DB接続はここでやっておく
on_worker_boot do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.establish_connection end end
prune_bundler: phased_restart時にbundlerのコンテキストを新しいものに切り替えてくれる
- これによりphased_restart(1つずつ再起動)した時に最新のGemfileを見に行ってくれる
- phased_restartを行いたい時にほぼ必須のオプション
- デフォルト: off
prune_bundler
plugin: プラグイン読み込み
plugin :tmp_restart
activate_control_app: コントロールサーバのURL
- pumactlコマンドでpumaを操作するのに利用する
- 詳しくは コントロールサーバ を参照
# デフォルト: localhostのport 9293 activate_control_app # unixソケット activate_control_app 'unix:///var/run/pumactl.sock' # tokenによる認証 # トークンを指定すると、クエリパラメータにそのトークンを指定しなければいけなくなる activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' } activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true }
state_path: stateファイルのパス
- stateファイルはサーバの状態の情報
- pumactlコマンドでpumaを操作するのに利用する
state_path '/u/apps/lolcat/tmp/pids/puma.state' state_path "#{Dir.pwd}/tmp/pids/puma.state" # Railsの場合、tmp/pids/puma.stateかな
directory: 起動ディレクトリ
- defalut: カレントディレクトリ
directory '/u/apps/lolcat'
on_worker_shutdown: 各ワーカーのshutdown前の処理
on_worker_shutdown do puts 'On worker shutdown...' end
tag: プロセスリストでの追加情報
- tagを指定しない場合は、推測する
- tagを追加したくない場合は、空文字を指定する
tag 'app name'
worker_timeout: 全てのワーカーがマスタープロセスにチェックインする、タイムアウト時間
- タイムアウトしたら、workerはリスタートされる
- デフォルト: 60秒
worker_timeout 60
worker_boot_timeout: ワーカーのブートのタイムアウト時間
- デフォルト: worker_timeoutの値
worker_boot_timeout 60
lowlevel_error_handler{}: アプリ外の例外のエラーハンドリング
- デフォルト: 500とエラーテキストを返す
lowlevel_error_handler do |e| Rollbar.critical(e) [500, {}, ["An error has occurred, and engineers have been informed. Please reload the page. If you continue to have problems, contact support@example.com\n"]] end
app: Rackアプリ
- config自体がRackアプリになる
app do |env| puts env body = 'Hello, World!' [200, { 'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s }, [body]] end
rackup: アプリ起動ファイルのパス
- デフォルト: "config.ru"
rackup '/u/apps/lolcat/config.ru'
quiet: リクエストロギングをdisable
- デフォルト: false
quiet
log_requests: リクエストロギングをenable
restart_command: pumaの再起動に使用するコマンド
restart_command '/u/app/lolcat/bin/restart_puma'
load: 追加のconfigファイルをロード
on_restart{}: リスタート前の処理
- 例: ログファイルを閉じる、DBコネクションを閉じる、Redisコネクションを閉じる
on_restart do puts 'On restart...' end
persistent_timeout: persistent connectionsのタイムアウト
first_data_timeout: 受信無しでtcpソケットを開き続ける際のタイムアウト
tcp_mode!: pumaをTCPモードで起動
queue_requests: リクエストをキューする
- リクエストをキューすることで、一般的にはパフォーマンスが上がる
- 参考: https://github.com/puma/puma/blob/master/docs/architecture.md
- デフォルト: true
shutdown_debug: shutdown時にバックトレースを出力
debug: デバグ情報を出力
early_hints: Early Hintsのサポートを有効にする
他にも色々
プロセス管理
シグナル
- masterプロセスにシグナルを送ることで、pumaの停止/再起動等が可能
- pumactlコマンドがいい感じにラップしてくれてるので、そっちを使うのが良さそう
シグナル一覧
INT: 停止
- Ctrl+C
QUIT: 停止
TERM: 停止
USR2: 再起動
- 設定ファイルをリロードする
USR1: 再起動(Phased Restart)
- 設定ファイルをリロードしない
TTIN: ワーカー1増やす
TTOU: ワーカー1減らす
HUP: ログファイルをリオープン
- ログファイルは
stdout_redirect
の設定を見る stdout_redirect
がなければINTのようにふるまう
pumaコマンド
- pumaコマンドのオプションは設定ファイルに対応する項目がある。詳細については 設定項目 を参照
- ただし設定ファイルとは設定方法が微妙に違う項目もあるので注意
オプション
-C, --config PATH: 設定ファイル読み込み(loadに対応)
# デフォルト値(config/puma.rb) $ puma # 指定 $ puma -C config/puma.rb # ファイルを指定したくない場合(デフォルト値が使われるのを防ぐ) $ puma -C "-"
-b, --bind URI: バインド(bindに対応)
# TCPソケットを使う場合 $ puma -b tcp://127.0.0.1:9292 # UNIXソケットを使う場合 # TCPより若干パフォーマンスが上がる(場合がある) $ puma -b unix:///var/run/puma.sock # UNIXソケットのパーミッションを変更する必要がある場合、umaskパラメーターを利用する $ puma -b 'unix:///var/run/puma.sock?umask=0111' # SSLを利用する場合 $ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
-t, --threads INT: スレッド数(threadsに対応)
$ puma -t 8:32
-w, --workers COUNT: workers数(workersに対応)
$ puma -w 3
-d, --daemon: デーモン化(daemonizeとquietに対応)
-e, --environment ENVIRONMENT: 環境(environmentに対応)
-p, --port PORT: bind(portに対応)
-q, --quiet: リクエストロギングをdisable(quietに対応)
-v, --log-requests: リクエストロギングをenable(log_requestsに対応)
-R, --restart-cmd CMD: pumaの再起動に使用するコマンド(restart_commandに対応)
-S, --state PATH:stateファイルのパス(state_pathに対応)
--tcp-mode: TCPモード(tcp_mode!に対応)
--early-hints: Early Hintsのサポートを有効にする(early_hintsに対応)
--debug: デバグ情報を出力(debugに対応)
--dir DIR: directory: 起動時ディレクトリ(directoryに対応)
--pidfile PATH: PIDファイルパス(pidfileに対応)
--preload: プリロード(preload_app!に対応)
--prune-bundler: bundler切り替え(prune_bundlerに対応)
--tag NAME: プロセスリストでの追加情報(tagに対応)
--redirect-stdout FILE: 標準出力のリダイレクト先ファイル(stdout_redirectに対応)
--redirect-stderr FILE, 標準エラー出力のリダイレクト先ファイル(stdout_redirectに対応)
--[no-]redirect-append: 追記モード(stdout_redirectに対応)
--control-url URL: コントロールサーバのurl(activate_control_appに対応)
# pumaの場合 $ puma --control-url tcp://127.0.0.1:9293 --control-token foo # pumactlの場合 $ pumactl --control-url 'tcp://127.0.0.1:9293' --control-token foo restart
--control URL: --control-urlと同じ(activate_control_appに対応)
--control-token TOKEN: コントロールサーバの認証用トークン(activate_control_appに対応)
-I, --include PATH: $LOAD_PATHに追加
-V, --version: バージョン
-h --help: ヘルプ
pumactlコマンド
pumactl
- コントロールサーバを使ってpumaのプロセス管理を行うコマンド
- コントロールサーバのurlが指定されている場合は、HTTPリクエストを使う。urlが指定されていない場合は、シグナルを使う。
- url指定は、設定なら
activate_control_app
、オプションなら--control-url
- ctlはcontrolの略なんだね
- pumactlのメリット
- pumaのプロセス管理はpumaコマンド(起動/状態確認など)、シグナル(停止/再起動など)、PID/stateファイル管理などで色々面倒だった。pumactlコマンドでプロセス管理の統一的なインタフェースを用意したってことらしい。(参考: http://ruby-journal.com/digesting-pumactl/)
- 参考: https://github.com/puma/puma/blob/master/lib/puma/control_cli.rb
コントロールサーバ
- HTTPリクエスト受け付けて、pumaの再起動などを行うRackアプリのサーバ
- HTTPリクエストを送るにはpumactlコマンドを使う
- tokenを使えば簡易的な認証が可能
- 使い方
- 設定ファイルなら
activate_control_app
- pumaコマンドなら
--control-url
- 設定ファイルなら
- 参考: https://github.com/puma/puma/blob/master/lib/puma/app/status.rb
# puma + コントロールサーバ 起動 $ puma --control-url tcp://127.0.0.1:9293 --control-token foo # HTTPリクエストでpuma再起動 $ pumactl --control-url 'tcp://127.0.0.1:9293' --control-token foo restart
コマンド
start: 起動
$ pumactl start
stop: 停止(TERM)
$ pumactl stop
halt: 停止(QUIT)
# $ kill -QUIT `(cat tmp/pids/server.pid)` 相当
$ pumactl halt
restart: 再起動(USR2)
$ pumactl restart
phased-restart: 再起動(USR1)
- phased-restart
$ pumactl phased-restart
status: PID確認
$ pumactl status # 起動している場合 PID 2557 is running # 起動してない場合 No puma process
stats: worker等の情報
gc: ガベージコレクション開始
gc-stats: ガベージコレクションの情報
reload-worker-directory: ?
オプション
-S, --state PATH: stateファイルのパス
-F, --config-file PATH: 設定ファイルのパス
-P, --pidfile PATH: pidファイルのパス
-p, --pid PID: pid
-C, --control-url URL: control serverのurl
-T, --control-token TOKEN: control serverの認証トークン
-H, --help: ヘルプ表示
-V, --version: バージョン表示
-Q, --quiet: 表示を止める
railsコマンド
起動
$ rails s $ rails s Puma
結局どうすれば?
- 設定は全て設定ファイルに書いて、pumactlコマンド使うのが簡単そう。
その他メモ
2つのモード
Single mode: プロセスは1つだけ
- もともとはpumaはSingle modeのみでスレッド専用として開発された
Clustered mode: masterプロセス1つ + workerプロセス複数
- master-workerモデル
- 設定でworkersを指定するとこっちになる
- 本番環境ではClustered modeで使うべき
2つのリスタート
参考
Hot Restart: リスタート時に、サーバーのソケットを開きっぱなしにする
- 処理が完了するのを待ってから再起動する
- unicornとかのホットデプロイとは違うっぽい(ダウンタイムなしという意味ではPhased Restartが近い?)
メリット
- リクエストの取りこぼしがなくなる
- preloadと併用できる
デメリット
- ダウンタイムあり(リスタートが完了するまで、リクエスト処理は止まる)
使い方
- pumactl restart
- USR2シグナル
Phased Restart: 1つずつworkerを再起動する
- ワーカーを1つずつ再起動するので、どこかのワーカーがリクエスト処理をしてくれる
メリット
- ダウンタイムなし(常にどこかのworkerが生きているので、リクエスト処理が止まらない)
デメリット
- Clustered mode(復数worker)のみ
- preloadは使えない(workerを1つずつkillして、1つずつ再起動するため)
- DBスキーマは新しいのに、アプリコードは古いという自体になりうる(新しいコードと古いコードが共存するので)。なのでマイグレーションをするような環境ではHOT Restartを利用すべき。あるいは古いバージョンのアプリコードと互換性がないといけない。
使い方
- pumactl phased-restart
- USR1シグナル
プラグイン
- Pumaに機能を追加できる
- Puma3.0から導入
使い方
# Gemfile gem 'puma-heroku' # プラグイン
$ bundle install
# config/puma.rb plugin :heroku
有名プラグイン
tmp_restart: tmp/restart.txtをtouchするとリスタートする
heroku: heroku向けの設定を用意してくれる
puma-heroku
という別のgem- https://github.com/puma/puma-heroku/blob/master/lib/puma/plugin/heroku.rb
自分で実装
- configやstart等のフックメソッドをオーバーライドすれば実装できるっぽい
- 上の2つのプラグインを参考にする
worker数、thread数の目安
- 参考
worker数の目安
- CPUの観点
- とりあえずworker数 = CPUコア数。実際にはさらに最適化すべき。コア数以上にしたほうが良い場合もある。CPUコア数の1.5倍まで増やしてもいいかも
- cpu使用率70%くらいが目安
- メモリの観点
- worker数 = RAM / (1プロセスのメモリ使用量 * 1.2) # Railsアプリだと1プロセス200MB~400MB程度が基準
- メモリ使用量70%くらいが目安
- ロードバランシングの観点
- 最適なロードバランシングのためにはworker数は3以上
thread数の目安
- thread数決めるのは難しい。5程度にして後は忘れるのがいいという意見も。
- デフォは16。これはそこそこ妥当らしい。
- ちなみにMRIの場合はIOだけ並列化可能。これはだいたい総時間の10~25%程度らしい
- Unicornから移行する場合
- workerをunicornの半分にする + thread数を2にする -> これでメモリ使用量が50%になる
- スレッドに慣れてきたら、workerを減らしてthreadを増やして調整すると良い
puma + Systemd
- 本番環境ではSystemd使うと便利
- 参考
puma + Nginx
puma + heroku
- herokuのオススメサーバはpuma
- herokuはリバースプロキシがない。なので特にherokuではpumaが良さそう
- メモリが500MBの場合だと、unicornではきつかった記憶
- 参考
Rails5からRailsのデフォルト
- Rails5でActionCableの導入に伴い、development環境のアプリケーションサーバがWebrickからpumaへ変更
Mongrelから派生
- pumaはMongrelから派生した
- 改善点
- Rackアプリにした
- スレッドベースにした
プラットフォームによる成約
JRuby, Windows
restart時にソケットを必ず閉じる
ディスクリプタを受けわたせないため
cluster modeがない
fork(2)がないため
Windows
daemon modeがない
fork(2)がないため
puma関係のgem
puma_worker_killer: 定期的にworkerをkillするgem
- workerを長時間放置しておくと、メモリ使用量が徐々に増えてきて、サーバ全体の処理が遅くなる可能性がある。 -> puma_worker_killerを使い、定期的にworkerをkillして新しいworkerを立ち上げる
- Ruby Webアプリのプロセスは時間とともにメモリ使用量が増加する。生成後の2倍から3倍に達することもある。
- unicorn_worker_killerのpuma版
- 当然cluster modeでしか使えない
導入
1. インストール
# Gemfile gem 'puma_worker_killer'
$ bundle install
2. 設定
# config/puma.rb before_fork do PumaWorkerKiller.config do |config| # サーバのメモリ(1024MB) config.ram = 1024 # 確認頻度(5秒毎) config.frequency = 5 # workerを再起動する閾値(メモリ使用量が65%になったらkill) # 一番メモリ使用量の多いworkerを再起動する config.percent_usage = 0.65 # rolling_restartの頻度(12時間に1回) # rolling_restart: メモリ使用量に関係なく、順番にworkerを再起動する仕組み config.rolling_restart_frequency = 12 * 3600 # falseにすると監視ログを止める # 普通のログに混ざって紛らわしい config.reaper_status_logs = true end PumaWorkerKiller.start end
puma-dev: powのpuma版
- powのpuma版。powと同等の機能が利用できる
- 開発時のサーバとして利用すると便利
対応OS
- OS X
- Linux
powの知識
pros
- シンボリックリンクを貼るだけでバーチャルホストで切って開発できる
- "rails s"で起動することなく、アクセスすると自動で起動する
- 同一LAN内からアクセス可能
- 設定が不要(Zero-configuration)
cons
- もうメンテされていない
- 起動が遅い
- フォアグラウンドじゃないのでbinding.pryできない(pry-remoteやプロキシ使えばいける)
使い方
# ローカルからアクセス http://[project名].dev/ # 同一LAN内からアクセス http://[project名].[LANのIP-ADDRESS].xip.io
xip.io: IPアドレスをサブドメインに与えることでIPアドレス自身を返す、グローバルなDNS
- 37signalsがpowのために作ったらしい
- puma-devもxip.ioを利用できる
ワイルドカードDNS
- IPアドレスをサブドメインに与えることで、IPアドレス自身を返す、グローバルなDNS
- 例: "192.168.100.200.wild.card" は "192.168.100.200" を返す
powよりpuma-devを使うべき
- powがメンテされていない
- https対応
- WebSocket対応
- powder(pow用のコマンドツール)相当の機能が付属している
- MacとLinuxをサポート
導入(mac)
0. powをインストール済みの場合は、アンインストールする
- 色々問題があるっぽい
$ curl get.pow.cx/uninstall.sh | sh
1. brewでインストール
- gemでも可能
$ brew install puma/puma/puma-dev
2. DNS設定
- resolverを使い、ローカルのMacにpuma-dev用のDNSサーバ設定を追加
$ sudo puma-dev -setup
3. セットアップ
- ".dev"ではなく".test"にする
$ puma-dev -install -d test
4. シンボリックリンクを貼る
# puma-devコマンドを使う場合 $ cd my_app $ puma-dev link -n my_app # 自分でシンボリックリンク貼ってもokらしい $ cd ~/.puma-dev/ $ ln -s my_app
5. リスタート
touch tmp/restart.txt
デフォルトの設定
port: 80と443
domain: .test
- 昔は.devドメインだったが、2017年にGoogleが所有してからHSTS onlyになってしまったため、.testに変わった
- ChromeのVer.63から、.devにはHTTPSが必須
コマンド
puma-dev link: シンボリックリンクを貼る
# puma-devコマンドを使う場合 $ cd my_app $ puma-dev link -n my_app # 自分でシンボリックリンク貼ってもokらしい $ cd ~/.puma-dev/ $ ln -s my_app
touch tmp/restart.txt: アプリ再起動
puma-dev -stop: 全アプリを再起動
- puma-devにUSR1シグナルを送っている
# これと同じ $ pkill -USR1 puma-dev
puma-dev: 起動(フォアグラウンド)
- ~/.puma-devを使う
puma-dev -h: ヘルプ
sudo puma-dev -setup: 設定(macのみ)
puma-dev -pow: ~/.powを使う
- By default, puma-dev uses the domain .test to manage your apps. If you want to have puma-dev look for apps in ~/.pow, just run puma-dev -pow.
環境変数
環境変数一覧
CONFIG: pumaの設定ファイルのパス
- デフォルト: なし
- config/puma-dev.rbとかを指定する
THREADS: pumaのスレッド数
- デフォルト: 5
WORKERS: pumaのworker数
- デフォルト: 0
環境変数の読み込み順(上から順)
- ~/.powconfig
- .env
- .powrc
- .powenv
シンボリックリンク
- "~/.puma-dev/"配下にサブディレクトリを置けば、使える
- cool-frontend.testの場合、以下の2つを探す
- ~/.puma-dev/cool-frontend
- ~/.puma-dev/cool/frontend
HTTPS
- デフォルトでport443を使う
- 自動でオレオレ証明書を作ってくれるっぽい: "~/Library/Application Support/io.puma.dev/cert.pem."
ログ
- ログのパス: ~/Library/Logs/puma-dev.log
rest-clientの使い方 まとめ
- 感想とか
- リクエスト
- RestClient.get: GET
- RestClient.post: POST
- RestClient.delete: DELETE
- RestClient.patch: PATCH
- RestClient.put: PUT
- RestClient.head: HEAD
- RestClient.options: OPTIONS
- RestClient::Request.execute: HTTPメソッド指定
- 使い方
- 必須の引数
- オプションの引数
- :headers: リクエストヘッダ
- :cookies: クッキー
- :user: ベーシック認証のuser
- :password: ベーシック認証のpassword
- :block_response: レスポンス時の処理
- :raw_response: Responseの代わりにRawResponseを使う
- :max_redirects: リダイレクト回数の上限値
- :proxy: プロキシのURI
- :verify_ssl: SSLのverifyの設定
- timeout: open_timeoutとread_timeoutを両方同時にセット
- open_timeout: コネクションを開くまでに待つ最大秒数
- read_timeout: データ読み込みまでに待つ最大秒数
- :before_execution_proc: リクエスト前のフック(proc)
- :ssl_client_cert: クライアント証明書
- :ssl_client_key: クライアント証明書の秘密鍵
- :ssl_ca_file: CA証明書ファイル
- :ssl_ca_path,: CA証明書ファイルを含むディレクトリ
- :ssl_cert_store: CA証明書を含む証明書ストア
- :ssl_verify_callback: 検証をフィルタするコールバック
- :ssl_verify_callback_warnings: trueなら警告
- :ssl_version: SSLバージョン
- :ssl_ciphers: 利用可能な共通鍵暗号の種類
- GETとPOSTでパラメータのとり方が違う
- リクエストはステータスコードによって挙動が異なる
- レスポンス
- RESTfulなリソース
- クエリパラメータ
- タイムアウト
- リダイレクト
- ファイル転送(ストリーミング)
- プロキシ
- クッキー
- SSL/TLS
- ログ
- レスポンスのコールバック
- リクエスト失敗例外
- フック
- restclientコマンド
- 落ち葉拾い
感想とか
注意点
- これは自分用のまとめを公開したものです。ドキュメント/ソースコードを見ただけで試していないコードも多いので、参考程度に。
感想
- ActiveResourceみたいな感じかと思ったら全然違った。普通のHTTPクライアントでRESTっぽさはあんまりない。
- Faradayやhttp.rbのがシンプルで使いやすそう。まぁでも有名なgemならどれでも十分良い気がする。
- 日本語情報は少ないので、情報探すならreadme -> ソースコードが良さそう。どちらも情報量多くて読みやすい。
pros
- ドキュメントがしっかりしてる
- RestClient::Resourceでリソースを表現できるのが便利そう
cons
- 日本語情報は少なめ。海外では人気だけど日本ではあんまり人気ない?
- 他のHTTPクライアントに比べると、APIがちょい複雑?getとpostでパラメータの取り方が変わったりするの、若干ややこしい気がする
リクエスト
RestClient.get: GET
# API RestClient.get(url, headers={}) # 基本 RestClient.get 'http://example.com/resource' # クエリパラメータ # クエリパラメータはheadersのparamsで指定する。ややこしい。 -> 将来変更の可能性あり RestClient.get 'http://example.com/resource', {params: {id: 50, 'foo' => 'bar'}} # リクエストヘッダ RestClient.get 'https://user:password@example.com/private/resource', {accept: :json}
RestClient.post: POST
- urlencodedされる
# API RestClient.post(url, payload, headers={}) # 基本 RestClient.post 'http://example.com/resource', {param1: 'one', nested: {param2: 'two'}} # raw payloads RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain' RestClient.post 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf' # json RestClient.post "http://example.com/resource", {'x' => 1}.to_json, {content_type: :json, accept: :json} # Multipart(ファイルあり) RestClient.post '/data', :myfile => File.new("/path/to/image.jpg", 'rb') # Multipart(ファイルなし) RestClient.post '/data', {:foo => 'bar', :multipart => true}
RestClient.delete: DELETE
RestClient.delete 'http://example.com/resource'
RestClient.patch: PATCH
RestClient.put: PUT
RestClient.head: HEAD
RestClient.head('http://example.com').headers
RestClient.options: OPTIONS
RestClient.options('http://example.com')
RestClient::Request.execute: HTTPメソッド指定
使い方
- .getとかではproxy等のオプションは利用できないっぽいので、そういう場合のみ.executeを使う
- .getメソッド等は内部で.executeを利用している
# RestClient.get('http://example.com/resource') と同じ RestClient::Request.execute(method: :get, url: 'http://example.com/resource') # オプション指定1 RestClient::Request.execute(method: :get, url: 'http://example.com/resource', timeout: 10) # オプション指定2 RestClient::Request.execute(method: :get, url: 'http://example.com/resource', ssl_ca_file: 'myca.pem', ssl_ciphers: 'AESGCM:!aNULL') # オプション指定3 RestClient::Request.execute(method: :delete, url: 'http://example.com/resource', payload: 'foo', headers: {myheader: 'bar'}) # GET http://example.com/resource?foo=bar # クエリパラメータはheadersのparamsに指定する RestClient::Request.execute(method: :get, url: 'http://example.com/resource', timeout: 10, headers: {params: {foo: 'bar'}})
必須の引数
:method: HTTPメソッド
:url: URL
オプションの引数
:headers: リクエストヘッダ
:cookies: クッキー
:user: ベーシック認証のuser
:password: ベーシック認証のpassword
:block_response: レスポンス時の処理
:raw_response: Responseの代わりにRawResponseを使う
:max_redirects: リダイレクト回数の上限値
- デフォルト: 10
:proxy: プロキシのURI
- RestClient.proxyよりもこっちが優先される
:verify_ssl: SSLのverifyの設定
- デフォルト: OpenSSL::SSL::VERIFY_PEER
timeout: open_timeoutとread_timeoutを両方同時にセット
- タイムアウト 参照
open_timeout: コネクションを開くまでに待つ最大秒数
read_timeout: データ読み込みまでに待つ最大秒数
:before_execution_proc: リクエスト前のフック(proc)
- RestClient.add_before_execution_proc相当
:ssl_client_cert: クライアント証明書
Net::HTTP#cert=
に対応
:ssl_client_key: クライアント証明書の秘密鍵
Net::HTTP#key=
に対応
:ssl_ca_file: CA証明書ファイル
Net::HTTP#ca_file
に対応
:ssl_ca_path,: CA証明書ファイルを含むディレクトリ
Net::HTTP#ca_path
に対応
:ssl_cert_store: CA証明書を含む証明書ストア
Net::HTTP#cert_store
に対応ca_file/ca_pathより細かく設定したい場合はコレを使う
:ssl_verify_callback: 検証をフィルタするコールバック
Net::HTTP#verify_callback
に対応
:ssl_verify_callback_warnings: trueなら警告
:ssl_version: SSLバージョン
Net::HTTP#ssl_version=
に対応
:ssl_ciphers: 利用可能な共通鍵暗号の種類
Net::HTTP#ciphers=
に対応
GETとPOSTでパラメータのとり方が違う
- POST/PUT/PATCHはpayloadを取る。GET/DELETE/HEAD/OPTIONSはpayloadを取らない
- パラメータの指定方法も異なるので注意する
# API RestClient.get(url, headers={}) RestClient.post(url, payload, headers={}) # パラメータ指定 RestClient.get 'http://example.com', {params: {foo: "bar"}} # GET http://example.com?foo=bar RestClient.post 'http://example.com'', {}, {params: {foo: "bar"}} # POST http://example.com foo=bar RestClient.post 'http://example.com', {foo: "bar"} # POST http://example.com foo=bar(payload)
リクエストはステータスコードによって挙動が異なる
200系: responseオブジェクトを返す
300系: 自動でリダイレクト
400系/500系: 例外を投げる
- rescueして個別に処理する。
レスポンス
response.code: ステータスコード
response = RestClient.get 'http://example.com/resource' # => <RestClient::Response 200 "<!doctype h..."> response.code # => 200
response.cookies: クッキー
response.cookies # => {"Foo"=>"BAR", "QUUX"=>"QUUUUX"}
response.headers: レスポンスヘッダ
response.headers # => {:content_type=>"image/jpg; charset=utf-8", :cache_control=>"private" ... } response.headers[:content_type] # => 'image/jpg'
resopnse.raw_headers: レスポンスヘッダ(raw)
response.body: レスポンスボディ
response.body # => "<!doctype html>\n<html>\n<head>\n <title>Example Domain</title>\n\n ..."
response.to_s: レスポンスボディ(alias)
response.request: リクエストオブジェクト
- RestClient::Requestオブジェクト
response.request.url # => "http://httpbin.org/get"
response.cookie_jar: ?
response.history: リダイレクト時のレスポンスの配列
RESTfulなリソース
- RestClient::ResourceはRESTfulなリソースを表現できる
- ネストされたリソースなどに繰り返しアクセスしたい場合に便利
GET
resource = RestClient::Resource.new('http://some/resource') resource.get
ネストされたリソース
# それぞれがResourceオブジェクトで、getメソッド等を使える site = RestClient::Resource.new('http://example.com') # http://example.com posts = site['posts'] # http://example.com/posts first_post = posts['1'] # http://example.com/posts/1 comments = first_post['comments'] # http://example.com/posts/1/comments site.get # GET http://example.com posts.get # GET http://example.com/posts first_post.get # GET http://example.com/posts/1 comments.get # GET http://example.com/posts/1/comments
第二引数はオプション
- RestClient::Request.executeのオプションと同じっぽい
# API RestClient::Resource.new(url, options={}, backwards_compatibility=nil, &block) # リクエストヘッダ resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 }) # タイムアウト resource = RestClient::Resource.new('http://slow', :read_timeout => 10) resource = RestClient::Resource.new('http://behindfirewall', :open_timeout => 10) # BASIC認証 resource = RestClient::Resource.new('http://protected/resource', {:user => 'user', :password => 'password'})
クエリパラメータ
GET
# GET "https://httpbin.org/get?foo=bar&baz=qux" # クエリパラメータはheadersのparamsで指定する。ややこしい。 -> 将来変更の可能性あり RestClient.get('https://httpbin.org/get', params: {foo: 'bar', baz: 'qux'})
GET(Railsスタイルの配列)
- デフォルトはRailsのスタイル(?)と同じ
# GET "https://http-params.herokuapp.com/get?foo[]=1&foo[]=2&foo[]=3" response = RestClient.get('https://http-params.herokuapp.com/get', params: {foo: [1,2,3]})
GET(フラットな配列)
- フラットな配列が欲しい場合は、RestClient::ParamsArrayを使う
# GET "https://httpbin.org/get?foo=1&foo=2" RestClient.get('https://httpbin.org/get', params: RestClient::ParamsArray.new([[:foo, 1], [:foo, 2]]))
GET(入れ子のハッシュ)
# GET "https://http-params.herokuapp.com/get?outer[foo]=123&outer[bar]=456" response = RestClient.get('https://http-params.herokuapp.com/get', params: {outer: {foo: 123, bar: 456}})
POST
# POST "https://httpbin.org/post", data: "foo=bar&baz=qux" RestClient.post('https://httpbin.org/post', {foo: 'bar', baz: 'qux'})
POST(JSON payload)
- rest-clientはJSONをそのままでは扱えないので、自分でJSON文字列にする必要がある
payload = {'name' => 'newrepo', 'description': 'A new repo'} RestClient.post('https://api.github.com/user/repos', payload.to_json, content_type: :json)
タイムアウト
timeout: open_timeoutとread_timeoutを両方同時にセット
- デフォルトは60s
- nilにするとタイムアウトしなくなる
- open_timeoutとread_timeoutを使うとより細かく指定できる
RestClient::Request.execute(method: :get, url: 'http://example.com/resource', timeout: 120)
open_timeout: コネクションを開くまでに待つ最大秒数
RestClient::Request.execute(method: :get, url: 'http://example.com/resource', read_timeout: 120, open_timeout: 240)
read_timeout: データ読み込みまでに待つ最大秒数
リダイレクト
基本的な使い方
# ステータスコードが300台の場合は、自動でリダイレクトしてくれる response = RestClient.get('http://httpbin.org/redirect/2') # response.historyで、リダイレクトによる一連のレスポンスにアクセスできる response.history # => [<RestClient::Response 302 "<!DOCTYPE H...">, <RestClient::Response 302 "">] # `max_redirects: 0`で自動リダイレクトしないようにできる RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0) # `RestClient::Found: 302 Found`例外が投げられる # 手動リダイレクト begin # 自動リダイレクトしないようにする RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0) rescue RestClient::ExceptionWithResponse => err # 例外オブジェクト err # => #<RestClient::Found: 302 Found> # 例外を通してresponseにアクセスできる err.response # => RestClient::Response 302 "<!DOCTYPE H..." err.response.headers[:location] # => "/get" # 手動リダイレクト err.response.follow_redirection # => RestClient::Response 200 "{\n "args":..." end
POSTのリダイレクト
- 自動リダイレクトはGET/HEADだけ。POSTとかは自分で.follow_redirectionを使って手動リダイレクトする
- いくつか方法がある。例外スタイルがおすすめらしい
例外スタイルの場合
- リダイレクト系の例外をrescueして、手動リダイレクト
begin RestClient.post('http://example.com/redirect', 'body') rescue RestClient::MovedPermanently, RestClient::Found, RestClient::TemporaryRedirect => err err.response.follow_redirection # 例外(301, 302, 307)の場合は、手動リダイレクト end
例外スタイル + ステータスコードで分岐の場合
- 例外をまるごとrescueして、ステータスコードで分岐からの、手動リダイレクト
begin RestClient.post('http://example.com/redirect', 'body') rescue RestClient::ExceptionWithResponse => err # 例外(301, 302, 307)の場合は、手動リダイレクト case err.http_code when 301, 302, 307 err.response.follow_redirection else raise end end
ブロックスタイルの場合
- ブロックでresponse時の処理を指定できる。そこでステータスコードで分岐からの、手動リダイレクト
RestClient.post('http://example.com/redirect', 'body') { |response, request, result| case response.code when 301, 302, 307 response.follow_redirection # 手動リダイレクト else response.return! # デフォルトの処理 end }
ファイル転送(ストリーミング)
リクエスト
# ファイルアップロード(text/plain) RestClient.put('http://httpbin.org/put', File.open('/tmp/foo.txt', 'r'), content_type: 'text/plain') # ファイルアップロード(multipart/form-data) RestClient.put('http://httpbin.org/put', {file_a: File.open('a.txt', 'r'), file_b: File.open('b.txt', 'r')})
レスポンス
- RestClient.getの時、レスポンスはメモリにバッファされる。しかしisoなどの大きいデータの場合は、メモリに収まらないのでファイルに直接ストリームしたい。 -> 2つの方法がある
raw_response: Tempfileに保存する
raw_response: true
にすると、レスポンスをTempfileに保存する- RestClient::Responseオブジェクトの代わりに、RestClient::RawResponseオブジェクトを返す
raw_response = RestClient::Request.execute( method: :get, url: 'http://releases.ubuntu.com/16.04.2/ubuntu-16.04.2-desktop-amd64.iso', raw_response: true ) # => <RestClient::RawResponse @code=200, @file=#<Tempfile:/tmp/rest-client.20170522-5346-1pptjm1>, @request=<RestClient::Request @method="get", @url="http://releases.ubuntu.com/16.04.2/ubuntu-16.04.2-desktop-amd64.iso">> raw_response.file.size # => 1554186240 raw_response.file.path # => "/tmp/rest-client.20170522-5346-1pptjm1"
block_response: procを使い自分でファイルに書き込む
File.open('/some/output/file', 'w') {|f| # proc # responseはNet::HTTPのNet::HTTPResponseオブジェクト。これを使い、自分でファイルに書き込む block = proc { |response| response.read_body do |chunk| f.write chunk end } # リクエスト RestClient::Request.execute(method: :get, url: 'http://example.com/some/really/big/file.img', block_response: block) }
プロキシ
グローバル
# プロキシサーバを利用する RestClient.proxy = "http://proxy.example.com/" RestClient.get "http://some/resource" # http_proxy環境変数を使う場合は、自分でしていしないといけないっぽい? RestClient.proxy = ENV['http_proxy']
リクエスト毎
# このリクエストだけ、プロキシサーバを利用する RestClient::Request.execute(method: :get, url: 'http://example.com', proxy: 'http://proxy.example.com') # このリクエストだけ、プロキシサーバを利用しない RestClient.proxy = "http://proxy.example.com/" RestClient::Request.execute(method: :get, url: 'http://example.com', proxy: nil)
クッキー
get
response = RestClient.get 'http://example.com/action_which_sets_session_id' response.cookies # => {"_applicatioN_session_id" => "1234"}
set
- 後方互換性のため、headersとして指定可能
- クッキーとして使えるオブジェクトは3種類
- Hash{String/Symbol => String}
- ArrayHTTP::Cookie
- HTTP::CookieJar(http-cookie gemの機能)
# hashとして指定 # Set-Cookie: foo=Value; Domain=.example.com; Path=/ # Set-Cookie: bar=123; Domain=.example.com; Path=/ RestClient::Request.new( url: 'http://example.com', method: :get, cookies: {:foo => 'Value', 'bar' => '123'} ) # HTTP::CookieJarとして指定 jar = HTTP::CookieJar.new jar.add HTTP::Cookie.new('foo', 'Value', domain: 'example.com', path: '/', for_domain: false) RestClient::Request.new(..., :cookies => jar) # postで利用(headersとして指定可能) response2 = RestClient.post( 'http://localhost:3000/', {:param1 => "foo"}, {:cookies => {:session_id => "1234"}} )
SSL/TLS
# HTTPSでは、自動でシステムのCA証明書を使う RestClient.get 'https://user:password@example.com/private/resource' # クライアント証明書を指定 RestClient::Resource.new( 'https://example.com', :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")), :ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"), :ssl_ca_file => "ca_certificate.pem", :verify_ssl => OpenSSL::SSL::VERIFY_PEER ).get # SSLのエラーが起きる場合はverifyを無視できる # もちろんセキュリティー的に問題ありなので本番で使わないこと RestClient.get 'https://user:password@example.com/private/resource', verify_ssl: false
ログ
グローバル
- logの指定方法は4つ
- loggerオブジェクト
- ファイル名
- 'stdout'
- 'stderr'
RestClient.log = 'stdout' # 環境変数でもOK ENV['RESTCLIENT_LOG'] = 'stdout'
リクエスト単位
# logオプションを使う resource = RestClient::Resource.new 'http://example.com/resource', log: Logger.new(STDOUT) RestClient::Request.execute(method: :get, url: 'http://example.com/foo', log: Logger.new(STDERR))
レスポンスのコールバック
基本
- ブロックを使うと、レスポンスのコールバックを指定できる
- ブロックの戻り値が、戻り値になる
RestClient.get('http://example.com/nonexistent') # 例外を投げる RestClient.get('http://example.com/nonexistent') {|response, request, result| response } # => <RestClient::Response 404 "<!doctype h...">
ステータスコードで処理を分岐
- ブロックではデフォルトの処理をしなくなるので、デフォルトの処理をしてほしい場合は
response.return!(&block)
を行う - ブロックを使う方法よりも、rescueを使う方法のほうが自然でおすすめらしい
# ブロックを使う方法 RestClient.get('http://example.com/resource') { |response, request, result, &block| case response.code when 200; response # 200の場合は、レスポンスをそのまま返す when 423; raise SomeCustomExceptionIfYouWant # 423の場合は、指定の例外 else ; response.return!(&block) # その他の場合は、デフォルトの処理 end } # rescueを使う方法(こっちのが自然でおすすめらしい) begin # リクエスト response = RestClient.get('http://example.com/resource') rescue RestClient::Unauthorized, RestClient::Forbidden => err # エラー1の場合 puts 'Access denied' return err.response rescue RestClient::ImATeapot => err # エラー2の場合 puts 'The server is a teapot! # RFC 2324' return err.response else # 成功の場合 puts 'It worked!' return response end
リクエスト失敗例外
リクエスト失敗例外の継承階層
- リクエスト時はこれらの例外をrescueする
- ステータスコード単位なら、RestClient::NotFoundなどをrescueする。まとめてrescueしたいならRestClient::RequestFailedをrescueする
RuntimeError: Rubyの例外 RestClient::Exception: RestClientのベースとなる例外 RestClient::ExceptionWithResponse: リクエスト失敗時の例外(互換性のため存在) RestClient::RequestFailed: リクエスト失敗時の例外 RestClient::NotFound等: ステータスコードに対応した例外(404なら、RestClient::NotFound)
リクエスト失敗例外をキャッチする
# キャッチしない RestClient.get 'http://example.com/nonexistent' # => RestClient::NotFoundが投げられる # キャッチする begin RestClient.get 'http://example.com/nonexistent' rescue RestClient::NotFound => exception exception.response # => <RestClient::Response 404 "<!doctype h..."> end
exception.response: レスポンス
exception.http_code: ステータスコード
exception.http_headers: レスポンスヘッダ
exception.http_body: レスポンスボディ
フック
- RestClient.add_before_execution_procを使うと、executionメソッドの実行前フックを仕掛けることができる
- :before_execution_procオプションで、リクエスト単位での実行も可能っぽい
RestClient.add_before_execution_proc do |request, params| # 任意の処理 end # リクエスト前に任意の処理が実行される RestClient.get 'http://example.com'
restclientコマンド
- gem installすると、シェルで使えるrestclientというコマンドがついてくる。
- そんなに便利ではなさそう。使わないかなー。
rest-clientをrequireした状態で、irbを起動
$ restclient >> RestClient.get 'http://example.com'
リソース指定
$ restclient http://example.com >> put '/resource', 'data'
認証
$ restclient https://example.com user pass >> delete '/private/resource'
設定ファイルを使う
1.設定ファイルを作成
# ~/.restclient sinatra: url: http://localhost:4567 rack: url: http://localhost:9292 private_site: url: http://example.com username: user password: pass
2.設定ファイルを読み込み
# $ restclient https://example.com user pass と同じ $ restclient private_site
単発のリクエスト
$ restclient get http://example.com/resource > output_body $ restclient put http://example.com/resource < input_body
落ち葉拾い
requireの方法は2つ
# 推奨 require 'rest-client' # 非推奨(後方互換性のため残ってる) require 'rest_client'
その他
- APIはSinatraのDSLに影響を受けた
- Ruby 2.0+
- v2.0でAPIが結構変わったので注意
参考サイト
Faradayの使い方 59のレシピ
- 第1章 Faradayをはじめよう
- 第2章 基本的な使い方
- 第3章 Faradayミドルウェアを使う
- 第4章 Faradayミドルウェア一覧
- 030 Faradayミドルウェア一覧
- 031 Faraday::Request::UrlEncoded - リクエストパラメータをURLエンコードする
- 032 Faraday::Request::Multipart - ファイルアップロード時にマルチパートでデータ送信する
- 033 Faraday::Request::BasicAuthentication - ベーシック認証
- 034 Faraday::Request::TokenAuthentication - トークン認証
- 035 Faraday::Request::Authorization - Authorizationヘッダーをセット
- 036 Faraday::Request::Retry - 失敗時にリトライする
- 037 Faraday::Response::Logger - リクエスト/レスポンス情報のログ吐き
- 038 Faraday::Response::RaiseError - 特定のステータスコードで、例外を投げる
- 039 faraday_middlewareを使う
- 040 FaradayMiddleware::OAuth - OAuth
- 041 FaradayMiddleware::OAuth2 - OAuth2
- 042 FaradayMiddleware::EncodeJson - リクエストボディをJSONエンコードする
- 043 FaradayMiddleware::MethodOverride - X-Http-Method-Overrideヘッダを使い、POSTで他のHTTPメソッドを代用する
- 044 FaradayMiddleware::ParseJson - パース(JSON)
- 045 FaradayMiddleware::ParseXml - パース(XML)
- 046 FaradayMiddleware::ParseYaml - パース(YAML)
- 047 FaradayMiddleware::ParseMarshal - パース(マーシャルデータ)
- 048 FaradayMiddleware::ParseDates - パース(時刻データ)
- 049 FaradayMiddleware::ParseJson::MimeTypeFix - パース(JSON) + MimeType修正
- 050 FaradayMiddleware::Mashify - パース(Hashie::Mash)
- 051 FaradayMiddleware::Rashify - パース(Hashie::Rash)
- 052 FaradayMiddleware::Chunked - パース(チャンク転送のデータ)
- 053 FaradayMiddleware::Caching - レスポンスをキャッシュする
- 054 FaradayMiddleware::FollowRedirects - リダイレクト先をGETする
- 055 FaradayMiddleware::Gzip - レスポンスbodyをGzip解凍する
- 056 FaradayMiddleware::Instrumentation - ActiveSupport::Notificationsを使い、リクエストを計測する
- 057 faraday_middleware-parse_oj: JSONのパースにojを使う
- 058 faraday-cookie_jar: クッキーを扱う
- 059 faraday-detailed_logger: いい感じのログ
- 付録
- Faradayの情報源
- 日本語の情報源
- Web Clients for Ruby and What they should be in the future
- Webuilder240's Blog - FaradayでHTTPリクエストを並列で実行する方法
- ローファイ日記 - Faradayの話 - OpenStack クライアント開発日記 (3)
- Ruby の HTTP Client「Faraday」を使った場合の例外の扱いとリトライ処理をどうするか考えてみた
- Developers.IO - Faraday の スタブテスト
- Faradayを使ったプログラムをRspecでテスト(Railsでない)
- Apitore blog - RubyでAPIコールするならFaradayが簡単便利
- Sarabande.jp - Ruby: Faraday を使って HTTP リクエストを送信する
- 成らぬは人の為さぬなりけり - Faradayを触ってみた
- Ruby の HTTP クライアントライブラリ Faraday が便利そう
- 英語の情報源
- 日本語の情報源
- Faradayの情報源
第1章 Faradayをはじめよう
001 Faradayとは?
🐱 今日はFaradayっていうHTTPクライアントのgemを紹介させてね。
👦🏻 ふぁらでい?
🐱 FaradayはRackミドルウェアに似たミドルウェア機能が特徴で、ミドルウェアを追加することで色々な機能を追加できるようになるよ。
👦🏻 らっく?みどるうぇあ?
🐱 Rackやミドルウェアについてはおいおい説明していくよ。とりあえず使ってみよう(๑´ڡ`๑)
002 セットアップ
🐱 まずはGemfileにfaradayを追加してね。
gem 'faraday'
🐱 次にbundle install
コマンドを実行すると、RailsアプリケーションでFaradayが使えるようになるよ。
$ bundle install
🐱 Rails以外で使う場合はgem install
すればOKだよ。
$ gem install faraday
003 使ってみよう
🐱 こんな感じでGETリクエストを送れるよ。
Faraday.get("http://example.com")
🐱 シンプルでわかりやすいね。
第2章 基本的な使い方
004 GETリクエスト
🐱 GETリクエストはこんな感じだよ。
Faraday.get("http://example.com")
005 POSTリクエスト
🐱 POSTリクエストはこんな感じだよ。
# 第二引数はパラメータ Faraday.post("http://example.com", name: "chibi")
006 PATCHリクエスト
🐱 PATCHリクエストはこんな感じだよ。
# 第二引数はパラメータ Faraday.patch("http://example.com", name: "chibi")
007 DELETEリクエスト
🐱 DELETEリクエストはこんな感じだよ。
Faraday.delete("http://example.com")
🐱 他にもPUTリクエストやHEADリクエスト等も同じように送れるよ。
008 コネクションを使う
🐱 こんな感じで、明示的にコネクションを生成してからリクエストすることもできるよ。
connection = Faraday.new("http://example.com") connection.get
🐱 Faraday::Connection.new
でも同じ事ができるよ。
connection = Faraday::Connection.new("http://example.com") connection.get
🐱 こんな感じでオプションを取ることができるよ。
connection = Faraday.new("http://example.com", params: {page: 1}) connection = Faraday.new("http://example.com", proxy: "http://proxy.com" ) connection = Faraday.new(url: "http://example.com") # urlはオプションで渡すこともできる
オプション一覧
オプション | 解説 |
---|---|
params | パラメータ |
url | url |
headers | HTTPリクエストヘッダー |
request | リクエストのオプション |
ssl | SSLのオプション |
proxy | プロキシのオプション |
🐱 他にもこんな感じのメソッドがあるよ。
connection.params # パラメータ connection.headers # リクエストヘッダー connection.ssl # SSLのオプション connection.url_prefix # URL connection.proxy # プロキシのオプション connection.get # GETリクエスト connection.post # POSTリクエスト connection.patch # PATCHリクエスト connection.delete # DELETEリクエスト
009 パラメータを指定する
🐱 第二引数でパラメータを指定できるよ。
Faraday.get("http://example.com/cats", page: 2)
🐱 GETの場合は直接クエリパラメータを指定してもOKだよ。
Faraday.get("http://example.com/cats?page=2")
🐱 ブロックでセットすることも出来るよ。
connection = Faraday.new("http://example.com") connection.get "/cats" do |request| request.params[:page] = 2 end
🐱 こんな感じでセットすることも出来るよ。
connection = Faraday.new("http://example.com") connection.params[:page] = 2 connection.get("/cats")
010 リクエストヘッダを指定する
🐱 第三引数でリクエストヘッダを指定できるよ。
Faraday.get("http://example.com/cats", {"page" => 2}, {"Accept" => "application/json"})
🐱 ブロックでセットすることも出来るよ。
connection = Faraday.new("http://example.com") connection.get "/cats" do |request| request.headers["Accept"] = "application/json" end
🐱 こんな感じでセットすることも出来るよ。
connection = Faraday.new("http://example.com") connection.headers["Accept"] = "application/json" connection.get("/cats")
011 レスポンスを使う
🐱 レスポンスはこんな感じで利用できるよ。
response = Faraday.get("http://example.com") response.body # レスポンスbody response.headers # レスポンスheader response.status # ステータスコード response.success? # リクエストは成功か?(ステータスコードが200番台か?)
012 ミドルウェアを使う
🐱 Faradayにはミドルウェア機能があるよ。ミドルウェアを使うと、こんな感じでいろんな機能を追加できんだ。
# コネクション生成時にミドルウェアをセット connection = Faraday.new("http://example.com") do |builder| builder.request :url_encoded # リクエストパラメータをURLエンコードする builder.response :logger # リクエスト・レスポンスの内容を標準出力に出力する builder.adapter :net_http # net/httpをアダプタに使う end # GETリクエスト connection.get("/cats")
🐱 更に踏み込んだ使い方については 第3章 Faradayミドルウェアを使うを見てね。
013 アダプタを使う
🐱 実はFaraday自体は実際にはHTTPリクエストをしないんだ。リクエスト処理はNet::HTTPやHTTPClientなどの他のライブラリに任せているんだよ。これらをアダプタと言うよ。アダプタを切り替えることで、FaradayのAPI(メソッド)は同じまま、リクエスト部分のライブラリを変えることができるよ。
🐱 使いたいアダプタをinstallしておいてね。今回はアダプタにHTTPClientを使うよ。
# Gemfile gem 'faraday' gem 'httpclient'
$ bundle install
🐱 こんな感じで使ってね。
connection = Faraday.new("http://example.com") do |builder| builder.adapter :httpclient # アダプタにHTTPClientを利用 end
🐱 利用できるアダプタはこんな感じだよ。
アダプタ | シンボル |
---|---|
Net::HTTP | :net_http |
Net::HTTP::Persistent | :net_http_persistent |
Typhoeus | :typhoeus |
Patron | :patron |
EM-Synchrony | :em_synchrony |
EM-HTTP | :em_http |
Excon | :excon |
HTTPClient | :httpclient |
🐱 ちなみにデフォルトのアダプタはNet::HTTPだよ。並列リクエストとか特別なことをする場合以外は、Net::HTTPのままでいいと思うよ。
🐱 ここで2つ注意点があるよ。
🐱 1つ目はミドルウェアの追加時にアダプタは最後に置くこと。Faradayミドルウェアでは後から追加したミドルウェアが一番内側にくるんだ。アダプタは一番内側に来てほしいから、一番最後に指定する必要があるよ。
client = Faraday.new("http://example.com") do |builder| # ミドルウェア色々 builder.request :url_encoded builder.response :logger ... # アダプタは一番最後 builder.adapter :httpclient end
🐱 2つ目はミドルウェアを指定する際に、アダプタ指定が必須なこと。デフォルトのミドルウェアとして:url_encoded
と:net_http
の2つが指定されているんだけど、ミドルウェアを指定する際にデフォルトのミドルウェアは無効になるんだ。だから自分で明示的に指定する必要があるよ。忘れがちだから気をつけてね。
# bad # これだとアダプタが未選択になってしまう。 client = Faraday.new("http://example.com") do |builder| builder.request :url_encoded builder.response :logger end # good client = Faraday.new("http://example.com") do |builder| builder.request :url_encoded builder.response :logger # アダプタ指定は必須。デフォルトのNet::HTTPを使う場合は`Faraday.default_adapter`か`:net_http`を指定する builder.adapter Faraday.default_adapter end
014 アダプタの動作をカスタマイズする
🐱 Faradayはアダプタ間のAPIの違いを吸収してくれるから、各アダプタの使い方は気にせずにFaradayの使い方だけ覚えておけばいいよ。でもたまにアダプタ固有の機能を使いたくなることがあるよ。その場合にはブロックを使えば、アダプタを直接カスタマイズすることができるよ。
Net::Http
connection = Faraday.new("http://example.com") do |builder| builder.adapter :net_http do |http| # yields Net::HTTP http.idle_timeout = 100 end end
NetHttpPersistent
connection = Faraday.new("http://example.com") do |builder| builder.adapter :net_http_persistent do |http| # yields Net::HTTP::Persistent http.idle_timeout = 100 http.retry_change_requests = true end end
Patron
connection = Faraday.new("http://example.com") do |builder| builder.adapter :patron do |session| # yields Patron::Session session.max_redirects = 10 end end
HTTPClient
connection = Faraday.new("http://example.com") do |builder| builder.adapter :httpclient do |client| # yields HTTPClient client.keep_alive_timeout = 20 client.ssl_config.timeout = 25 end end
015 JSONをPOSTする
🐱 JSONをPOSTするにはこんな感じだよ。
connection = Faraday.new("http://example.com") connection.post("/cats.json") do |connection| # Content-Typeを指定 connection.headers["Content-Type"] = "application/json" # bodyにJSON文字列を指定 connection.body = {cat: {name: "tama"}}.to_json end
🐱 ミドルウェアを使えばもっと簡単にできるよ。
connection = Faraday.new("http://example.com") do |builder| # jsonミドルウェアを指定すると、いい感じにやってくれる。 builder.request :json builder.adapter Faraday.default_adapter end connection.post("/cats.json", {cat: {name: "tama"}})
016 SSLを使う
🐱 faradayではSSLを使う時に、認証局の証明書を指定する必要があるよ。ほとんどの場合、既にシステムにバンドルされているからそれを指定してあげてね。
🐱 ubuntuの場合は証明書のPATHはopensslコマンドで調べられるよ。
$ openssl version -d OPENSSLDIR: "/usr/lib/ssl/certs"
🐱 PATHはssl
オプションのca_path
で指定してね
connection = Faraday.new("https://example.com", ssl: { ca_path: "/usr/lib/ssl/certs" })
🐱 環境変数を使って指定することもできるよ。
ENV["SSL_CERT_PATH"] = "/usr/lib/ssl/certs" connection = Faraday.new("https://example.com")
🐱 SSLエラーが出る場合は、こんな感じでSSLの認証をスキップできるよ。でもセキュリティー的に難ありだから、デバッグ以外では使わない方が良いと思うよ。
connection = Faraday.new("https://example.com", ssl: { verify: false })
017 並列処理をする
🐱 並列(parallel)で処理したい場合はアダプタにtyphoeus
を使うといいよ。
# typhoeusのアダプタはtyphoeusライブラリにあるからrequireしてね require "typhoeus" require "typhoeus/adapters/faraday" connection = Faraday.new("http://example.com") do |builder| # アダプタにtyphoeusを指定 builder.adapter :typhoeus end response1, response2 = nil, nil # 並列処理 connection.in_parallel do # この2つのリクエストは並列に実行されるよ response1 = connection.get("/1") response2 = connection.get("/2") # この時点ではリクエストは完了していないよ。そのためbodyはnilになるよ response1.body # => nil response2.body # => nil end # ブロック終了後にはリクエストは完了しているよ。そのためbodyやstatusにアクセスできるよ response1.body # => 結果 response2.body # => 結果
🐱 参考 -> Webuilder240's Blog - FaradayでHTTPリクエストを並列で実行する方法
018 パラメータのシリアライズ方法を変更する
🐱 Faradayではデフォルトでクエリパラメータをids[]=1&ids[]=2
のような形でシリアライズするよ。Railsでよく見る形だね。
# GET http://example.com/cats?ids[]=1&ids[]=2 Faraday.get("http://example.com/cats", ids: [1, 2])
🐱 これはパラメータエンコーダーにデフォルトのFaraday::NestedParamsEncoder
が利用されているからだよ。明示的に書くとこうなるよ。
connection = Faraday.new("http://example.com/cats", request: { params_encoder: Faraday::NestedParamsEncoder }) # GET http://example.com/cats?ids[]=1&ids[]=2 connection.get { |request| request.params[:ids] = [1, 2] }
🐱 でも、どうもこの形はRailsでは標準だけどWeb全体の標準というわけではないらしいんだ。たとえばids=1&ids=2
で送りたい場合がある。この場合はパラメータエンコーダーにFaraday::FlatParamsEncoder
を利用すればいいよ。
connection = Faraday.new("http://example.com/cats", request: { params_encoder: Faraday::FlatParamsEncoder }) # GET http://example.com/cats?ids=1&ids=2 connection.get { |request| request.params[:ids] = [1, 2] }
🐱 こんな感じでコネクション毎に設定することもできるよ。
connection = Faraday.new("http://example.com/cats") do |builder| builder.request :url_encoded # optionsを通して設定する builder.options.params_encoder = Faraday::FlatParamsEncoder # アダプタ指定は必須 builder.adapter Faraday.default_adapter end # GET http://example.com/cats?ids=1&ids=2 connection.get{ |request| request.params[:ids] = [1, 2] }
🐱 エンコーダーは自分で実装することもできるよ。その場合はencode(params)
とdecode(query)
を実装したクラスを用意してね。詳しくはFaraday パラメータのエンコードを差し替えてみるがとっても参考になるよ。
019 ファイルをアップロードする
🐱 ファイルのアップロードだよ。multipart
ミドルウェアを使って、ContentTypeをmultipart/form-dataにするのがポイントだよ。
connection = Faraday.new("http://example.com") do |builder| # `multipart`ミドルウェアを使って、ContentTypeをmultipart/form-dataにする builder.request :multipart builder.request :url_encoded builder.adapter Faraday.default_adapter end params = { # 画像ファイル picture: Faraday::UploadIO.new("cat.jpg", "image/jpeg") } connection.put("/foo.json", params)
020 プロキシを使う
🐱 Faradayでは内部的にURI::Generic#find_proxy
を使って、環境変数http_proxy
などからプロキシを推測してくれるよ。
ENV['http_proxy'] = "http://proxy.com" Faraday.get('http://www.example.com/')
🐱 ドキュメントはこれだよ。 -> https://docs.ruby-lang.org/ja/latest/method/URI=3a=3aGeneric/i/find_proxy.html
🐱 環境変数を無視したい場合はこうするよ。
Faraday.ignore_env_proxy = true
🐱 こんな感じでオプションで指定することもできるよ。
Faraday.new("http://www.example.com", proxy: "http://proxy.com") Faraday.new("http://www.example.com", proxy: { uri: "http://proxy.example.com", user: "foo", password: "bar" })
021 タイムアウトを指定する
🐱 リクエストのタイムアウトは、オプションのopen_timeout
とtimeout
で指定するよ。open_timeout
がコネクションを開くまでに待つ最大秒数で、timeout
がデータ読み込みまでに待つ最大秒数だよ。
connection = Faraday.new('http://example.com') do |builder| builder.options[:open_timeout] = 2 # コネクションを開くまでに待つ最大秒数 builder.options[:timeout] = 5 # データ読み込みまでに待つ最大秒数 builder.adapter Faraday.default_adapter end
022 リクエストをスタブしてテストする
🐱 アダプタにFaraday::Adapter::Test::Stubs
を使うことで、リクエストをスタブできるよ。
# スタブを生成する stubs = Faraday::Adapter::Test::Stubs.new do |stub| # 配列は`[ステータスコード, レスポンスヘッダ, レスポンスボディ]` stub.get("/a") { |env| [200, {}, "1"] } end # stubsを使って、コネクションを生成する # さらに追加でスタブされたリクエストを追加している connection = Faraday.new do |builder| builder.adapter :test, stubs do |stub| stub.get("b") { |env| [ 200, {}, "2" ] } end end # コネクション生成後にも、スタブされたリクエストを追加できる # ここでは3つ目のスタブされたリクエストを追加してる stubs.get("/c") { |env| [ 200, {}, "3" ] } # GETリクエストをすると、指定したレスポンスが返される connection.get("/a").body # => "1" connection.get("/b").body # => "2" connection.get("/c").body # => "3" connection.get("/d") # このpathはスタブがないのでエラーになる
🐱 Railsのコントローラーをテストする際はこんな感じになるよ。
コントローラー
class FoobarController < ApplicationController def index # コネクションはスタブ化しやすいようにprivateメソッドに切り出しておく connection.get("foo") render nothing: true end private def connection Faraday.new("http://www.example.com") do |connection| connection.request :url_encoded connection.response :logger connection.adapter Faraday.default_adapter end end end
テスト
RSpec.describe FoobarController, type: :controller do # indexアクションのテスト describe "GET index" do # スタブ化されたコネクションを生成 let!(:stub_connection) do Faraday.new do |connection| connection.adapter :test, Faraday::Adapter::Test::Stubs.new do |stub| stub.get("foo") do [ 200, {}, JSON.generate([ { id: 1, name: "Foo" }, { id: 2, name: "Bar" } ]) ] end end end end # リクエスト let(:request) { get :index, format: :json } # `controller.connection # => stub_connection`となるようにスタブ化して、リクエストを発行 before do allow(controller).to receive(:connection).and_return(stub_connection) request end # リクエストが成功することをテスト it { expect(response).to be_success } end end
🐱 参考 -> Faraday の スタブテスト
第3章 Faradayミドルウェアを使う
023 ミドルウェアを使う
🐱 Faradayはミドルウェアを追加することでいろんな機能を追加することができるよ。
# コネクション生成時にミドルウェアを追加 connection = Faraday.new("http://example.com") do |builder| builder.request :url_encoded # リクエストパラメータをURLエンコードする builder.response :logger # リクエスト・レスポンスの内容を標準出力に出力する builder.adapter :net_http # net/httpをアダプタに使う end # GETリクエスト connection.get("/cats.json")
024 2通りの書き方
🐱 ミドルウェアの追加方法は2つあるよ。シンボルを使う方法と、クラスを使う方法。どちらもやっていることは同じだけど、シンボル指定のほうがわかりやすいからシンボル指定をおすすめするよ。
1.シンボルを使う
connection = Faraday.new("http://example.com") do |builder| builder.request :url_encoded builder.response :logger builder.adapter :net_http end
2.クラスを使う
connection = Faraday.new("http://example.com") do |builder| builder.use Faraday::Request::UrlEncoded builder.use Faraday::Response::Logger builder.use Faraday::Adapter::NetHttp end
025 ミドルウェアにオプションを渡す
🐱 ミドルウェアにオプションを渡すには第2引数に指定してあげればOKだよ。
connection = Faraday.new do |builder| credentials = { :consumer_key => consumer_key, :consumer_secret => consumer_secret, :token => oauth_token, :token_secret => oauth_token_secret } builder.request :oauth, credentials ... end
🐱 参考 -> Ruby の HTTP クライアントライブラリ Faraday が便利そう
026 ミドルウェアを後から追加・削除する
🐱 ミドルウェアは後から追加・削除できるよ。
connection = Faraday.new # ミドルウェアスタックの末尾(一番内側)に追加 connection.response :logger # loggerミドルウェアを削除 connection.builder.delete(Faraday::Response::Logger) # 0番目(ミドルウェアスタックの先頭)に追加 connection.builder.insert(0, Faraday::Response::Logger) # 0番目のミドルウェアとloggerミドルウェアを置き換え connection.builder.swap(0, Faraday::Response::Logger)
027 デフォルトのミドルウェアに気をつける
🐱 Faradayはデフォルトでurl_encoded(リクエストパラメータをURLエンコード)
とnet_http(net_httpアダプタ)
の2つのミドルウェアが指定されているよ。
# デフォルトではurl_encodedとnet_httpの2つのミドルウェアを利用する Faraday.get("http://example.com") # こんなイメージ connection = Faraday.new do |builder| builder.request :url_encoded builder.adapter :net_http end connection.get("http://example.com")
🐱 ブロックでミドルウェアを指定するとデフォルトのミドルウェアは利用されないよ。アダプタの指定を忘れないようにね。
# bad connection = Faraday.new do |builder| builder.request :multipart end connection.get("http://example.com") # アダプタ指定がないので、警告が出る
028 ミドルウェアの順番に気をつける
🐱 ミドルウェアは宣言する順番に気をつけてね。Rackミドルウェアと同じで、最初のミドルウェアが一番外側に来て、最後のミドルウェアが一番内側に来るよ。この順番を変えると動作が変わっちゃう場合があるから注意してね。特にアダプタは一番内側に来てほしいから、一番最後に持ってきてね。
Faraday.new(...) do |builder| # ミドルウェア順番を変えると動作が変わる場合がある builder.request :multipart builder.request :url_encoded # アダプタは一番内側に来てほしいので、一番最後 builder.adapter :net_http end
029 ミドルウェアの自作
🐱 Faradayミドルウェアは自作できるよ。Faraday::Middleware
を継承したクラスにcall
メソッドを実装すれば自作ミドルウェアを作れるよ。
class MyMiddlewre < Faraday::Middleware def call(request_env) # リクエスト時の処理はここで行う # request_env[:method]などを使ってね # - method: HTTPメソッド(:get, :post, ...) # - url: リクエストURL(GETパラメータ含む) # - body: POST/PUTパラメータ # - request_headers: リクエストヘッダ @app.call(request_env).on_complete do |response_env| # レスポンス時の処理はここで行う # response_env[:status]などを使ってね # - status: HTTPステータス # - body: レスポンスボディ # - response_headers: レスポンスヘッダ end end end
🐱 レスポンス時の処理のみ実装したい場合はFaraday::Response::Middleware
でon_complete
を実装すれば簡単に自作ミドルウェアを作れるよ。
class MyMiddleware < Faraday::Response::Middleware def on_complete(response_env) # response時の処理はここで行う end end
🐱 register_middleware
でミドルウェアを登録すれば、利用時にシンボルでアクセスできるようになるよ。
# ミドルウェアを登録 Faraday::Request.register_middleware my1: -> { MyMiddlewre1 } Faraday::Response.register_middleware my2: -> { MyMiddlewre2 } Faraday::Middleware.register_middleware my3: -> { MyMiddlewre3 } # 利用 connection.request :my1 connection.response :my2 connection.use :my3
🐱 参考 -> Ruby の HTTP クライアントライブラリ Faraday が便利そう
第4章 Faradayミドルウェア一覧
030 Faradayミドルウェア一覧
🐱 Faradayミドルウェアはfaraday gemについてくるもの以外にも、別gemとして開発されているものがあるよ。特にfaraday_middlewareというgemには便利なミドルウェアが色々あるからおすすめだよ。
🐱 ここでは僕が便利そうだなーと思ったミドルウェアを、Gem別にまとめたよ。
faraday
タイプ | クラス | シンボル | 解説 |
---|---|---|---|
Request | Faraday::Request::UrlEncoded | :url_encoded | リクエストパラメータをURLエンコードする |
Request | Faraday::Request::Multipart | :multipart | ファイルアップロード時にマルチパートでデータ送信する |
Request | Faraday::Request::BasicAuthentication | :basic_auth | ベーシック認証 |
Request | Faraday::Request::TokenAuthentication | :token_auth | トークン認証 |
Request | Faraday::Request::Authorization | :authorization | Authorizationヘッダーをセット |
Request | Faraday::Request::Retry | :retry | 失敗時にリトライする |
Response | Faraday::Response::Logger | :logger | リクエスト/レスポンス情報のログ吐き |
Response | Faraday::Response::RaiseError | :raise_error | 特定のステータスコードで、例外を投げる |
faraday_middleware
タイプ | クラス | シンボル | 解説 |
---|---|---|---|
Request | FaradayMiddleware::OAuth | :oauth | OAuth |
Request | FaradayMiddleware::OAuth2 | :oauth2 | OAuth2 |
Request | FaradayMiddleware::EncodeJson | :json | リクエストボディをJSONエンコードする |
Request | FaradayMiddleware::MethodOverride | :method_override | X-Http-Method-Overrideヘッダを使い、POSTで他のHTTPメソッドを代用する |
Response | FaradayMiddleware::ParseJson | :json | パース(JSON) |
Response | FaradayMiddleware::ParseXml | :xml | パース(XML) |
Response | FaradayMiddleware::ParseYaml | :yaml | パース(YAML) |
Response | FaradayMiddleware::ParseMarshal | :marshal | パース(マーシャルデータ) |
Response | FaradayMiddleware::ParseDates | :dates | パース(時刻データ) |
Response | FaradayMiddleware::ParseJson::MimeTypeFix | :json_fix | パース(JSON) + MimeType修正 |
Response | FaradayMiddleware::Mashify | :mashify | パース(Hashie::Mash) |
Response | FaradayMiddleware::Rashify | :rashify | パース(Hashie::Rash) |
Response | FaradayMiddleware::Chunked | :chunked | パース(チャンク転送のデータ) |
Response | FaradayMiddleware::Caching | :caching | レスポンスをキャッシュする |
Response | FaradayMiddleware::FollowRedirects | :follow_redirects | リダイレクト先をGETする |
Middleware | FaradayMiddleware::Gzip | :gzip | レスポンスbodyをGzip解凍する |
Middleware | FaradayMiddleware::Instrumentation | :instrumentation | ActiveSupport::Notificationsを使い、リクエストを計測する |
faraday_middleware-parse_oj
タイプ | クラス | シンボル | 解説 |
---|---|---|---|
Response | FaradayMiddleware::ParseOj | :oj | ojを使ってJSONをパース |
faraday-cookie_jar
タイプ | クラス | シンボル | 解説 |
---|---|---|---|
Middleware | Faraday::CookieJar | :cookie_jar | クッキーを扱う |
faraday-detailed_logger
タイプ | クラス | シンボル | 解説 |
---|---|---|---|
Response | Faraday::DetailedLogger | :detailed_logger | いい感じのログ |
🐱 それぞれのミドルウェアの使い方については、以降のレシピで個別に解説していくよ。
031 Faraday::Request::UrlEncoded - リクエストパラメータをURLエンコードする
🐱 リクエストパラメータをURLエンコードして、Content-Typeをapplication/x-www-form-urlencoded
にセットしてくれるよ。
builder.request :url_encoded
032 Faraday::Request::Multipart - ファイルアップロード時にマルチパートでデータ送信する
🐱 ContentTypeをmultipart/form-dataにして、マルチパートでデータ送信してくれるよ。
connection = Faraday.new("http://example.com") do |builder| # `multipart`ミドルウェアを使って、ContentTypeをmultipart/form-dataにする builder.request :multipart builder.request :url_encoded builder.adapter Faraday.default_adapter end params = { # 画像ファイル picture: Faraday::UploadIO.new("cat.jpg", "image/jpeg") } connection.put("/foo.json", params)
033 Faraday::Request::BasicAuthentication - ベーシック認証
🐱 Authorizationヘッダを使って、ベーシック認証ができるよ
builder.request :basic_auth, "username", "password"
🐱 ヘルパーメソッドでも同じことができるよ。
builder.basic_auth "username", "password"
034 Faraday::Request::TokenAuthentication - トークン認証
🐱 Tokenヘッダーを使って、トークン認証ができるよ
builder.request :token_auth, "token"
🐱 ヘルパーメソッドでも同じことができるよ。
builder.token_auth "token"
035 Faraday::Request::Authorization - Authorizationヘッダーをセット
🐱 Authorizationヘッダーを自分でセットできるよ。
# ベーシック認証を自分で実装 builder.request :authorization, :Basic, Base64.encode64("username" + ":" + "passwd")
🐱 ヘルパーメソッドでも同じことができるよ。
builder.authorization :Basic, Base64.encode64("username" + ":" + "passwd")
036 Faraday::Request::Retry - 失敗時にリトライする
🐱 失敗時にリトライするように設定できるよ。
builder.request :retry, max: 2, interval: 0.05, interval_randomness: 0.5, backoff_factor: 2, exceptions: [CustomException, "Timeout::Error"]
🐱 オプションはこんな感じだよ。
オプション | 解説 | デフォルト値 |
---|---|---|
max | 最大リトライ回数 | 2 |
interval | リトライまでの待ち時間 | 0 |
interval_randomness | リトライまでのランダム待ち時間。0~1のfloat | 0 |
max_interval | 最大待ち時間 | Float::MAX |
backoff_factor | バックオフ(リトライ回数による待ち時間) | 1 |
exceptions | リトライする例外 | [Errno::ETIMEDOUT, "Timeout::Error", Error::TimeoutError, Faraday::Error::RetriableResponse] |
retry_if | ブロックの戻り値がTrueの場合リトライfaraday.request :retry, retry_if: ->(env, _exception) { !(400..499).include?(env.status) } |
->(env,exception) { false } |
methods | retryが有効なHTTPメソッド | [:delete, :get, :head, :options, :put] |
retry_block | リトライ後に任意の処理を実行faraday.request :retry, retry_block: ->(env, middleware_options, retries, exception) { do_something } |
Proc.new {} |
037 Faraday::Response::Logger - リクエスト/レスポンス情報のログ吐き
🐱 リクエスト/レスポンスの情報を出力してくれるよ。デフォルトでは標準出力を使うよ。
connection = Faraday.new("https://google.com") do |builder| builder.response :logger builder.adapter Faraday.default_adapter end # このタイミングでログ出力 connection.get
出力内容
I, [2018-08-31T07:21:38.436765 #57129] INFO -- request: GET https://google.com/ D, [2018-08-31T07:21:38.436920 #57129] DEBUG -- request: User-Agent: "Faraday v0.15.2" I, [2018-08-31T07:21:38.595954 #57129] INFO -- response: Status 301 D, [2018-08-31T07:21:38.596067 #57129] DEBUG -- response: location: "https://www.google.com/" content-type: "text/html; charset=UTF-8" date: "Thu, 30 Aug 2018 22:21:38 GMT" expires: "Sat, 29 Sep 2018 22:21:38 GMT" cache-control: "public, max-age=2592000" server: "gws" content-length: "220" x-xss-protection: "1; mode=block" x-frame-options: "SAMEORIGIN" alt-svc: "quic=\":443\"; ma=2592000; v=\"44,43,39,35\"" connection: "close"
🐱 第二引数でカスタムのloggerを設定できるよ。
logger = Logger.new("logfile.log") connection = Faraday.new("https://google.com") do |builder| builder.response :logger, logger builder.adapter Faraday.default_adapter end connection.get
038 Faraday::Response::RaiseError - 特定のステータスコードで、例外を投げる
🐱 ステータスコードが400番台、500番台の時に例外を投げるように設定できるよ。
builder.response :raise_error
投げられる例外は以下の通りだよ
ステータスコード | 例外 |
---|---|
404 | Faraday::Error::ResourceNotFound |
407 | Faraday::Error::ConnectionFailed |
400...600 | Faraday::Error::ClientError |
039 faraday_middlewareを使う
🐱 faraday_middleware
をインストールすると便利なミドルウェアが色々使えるようになるよ。
🐱 インストールはこんな感じだよ。
# Gemfile gem 'faraday' # faraday_middlewareがfaradayに依存しているので、なくてもOK gem 'faraday_middleware'
$ bundle install
🐱 使い方はFaraday gemのミドルウェアと同じだよ。
connection = Faraday.new("http://example.com") do |builder| builder.request :url_encoded # Faraday gemのミドルウェアと同じように使える builder.response :json builder.adapter :net_http end connection.get("/cats.json")
🐱 一部のミドルウェアは別のライブラリに依存しているから注意してね。
ミドルウェア | 依存ライブラリ |
---|---|
FaradayMiddleware::Instrumentation | activesupport |
FaradayMiddleware::OAuth | simple_oauth |
FaradayMiddleware::ParseXml | multi_xml |
FaradayMiddleware::ParseYaml | safe_yaml |
FaradayMiddleware::Mashify | hashie |
FaradayMiddleware::Rashify | rash_alt |
040 FaradayMiddleware::OAuth - OAuth
🐱 OAuthを使うにはこのミドルウェアを使ってね。
041 FaradayMiddleware::OAuth2 - OAuth2
🐱 OAuth2を使うにはこのミドルウェアを使ってね。
# "token"がクエリパラーメータにaccess_tokenとして追加される # "token"がAuthorizationリクエストヘッダに"Token token=<token_value>"として追加される builder.request :oauth2, "token"
🐱 token_type
オプションも利用できるよ。
# "token"がAuthorizationリクエストヘッダに"Bearer <token_value>"として追加される builder.request :oauth2, "token", token_type: :bearer
042 FaradayMiddleware::EncodeJson - リクエストボディをJSONエンコードする
🐱 JSON.dump()
を使って、リクエストボディをJSON文字列にエンコードしてくれるよ。
builder.request :json
043 FaradayMiddleware::MethodOverride - X-Http-Method-Overrideヘッダを使い、POSTで他のHTTPメソッドを代用する
🐱 Railsアプリでよく見るやつだね。PATCHメソッドやDELETEメソッドが送れない時のために、DELETEなどのHTTPメソッドをX-Http-Method-Override
ヘッダとPOSTメソッドで代用するよ。Railsアプリ(Rackアプリ)では、デフォルトでHTTPメソッドをちゃんと解釈してくれるよ。
# 全てのHTTPメソッドを対象にする builder.request :method_override # PATCH、OPTIONSメソッドだけ対象にする builder.request :method_override, rewrite: [:patch, :options]
044 FaradayMiddleware::ParseJson - パース(JSON)
🐱 レスポンスボディをJSON.parse()
してくれるよ。
builder.response :json
🐱 content_type
オプションで、パースするレスポンスのContent-Typeを指定できるよ。これを指定しない場合、全てのレスポンスをJSONとしてパースしちゃうよ。
builder.response :json, :content_type => "application/json"
🐱 JSONでもContent-Typeがapplication/vnd.github.beta+json
のような場合もあるから、正規表現で指定しておくと無難だよ。
# \b: 単語の境界 # $: 行末 builder.response :json, :content_type => /\bjson$/
🐱 parser_options
を使うとパーサーにオプションを渡せるよ。
# `JSON.parse(response.body, symbolize_names: true)`のイメージ builder.response :json, parser_options: { symbolize_names: true }
045 FaradayMiddleware::ParseXml - パース(XML)
🐱 レスポンスボディをXMLとしてパースするよ。multi_xml
に依存してるよ。
builder.response :xml, :content_type => /\bxml$/
046 FaradayMiddleware::ParseYaml - パース(YAML)
🐱 レスポンスボディをYAMLとしてパースするよ。
builder.response :yaml
047 FaradayMiddleware::ParseMarshal - パース(マーシャルデータ)
🐱 マーシャル化されたRubyオブジェクトにMarshal.load()
を使うよ。
builder.response :marshal
048 FaradayMiddleware::ParseDates - パース(時刻データ)
🐱 時刻データをTime.parse()
を使ってパースするよ。
builder.response :dates
049 FaradayMiddleware::ParseJson::MimeTypeFix - パース(JSON) + MimeType修正
🐱 レスポンスbodyがjsonっぽかったら、レスポンスheaderのContent-Typeを"application/json"に書き換えるよ。JSONなのに"text/javascript"とかでリクエストを返すようなAPIに利用するよ。
builder.response :json_fix
050 FaradayMiddleware::Mashify - パース(Hashie::Mash)
🐱 配列かハッシュの場合、Hashie::Mashにパースするよ。
builder.response :mashify
051 FaradayMiddleware::Rashify - パース(Hashie::Rash)
🐱 配列かハッシュの場合、Hashie::Rashにパースするよ。
builder.response :rashify
052 FaradayMiddleware::Chunked - パース(チャンク転送のデータ)
🐱 Transfer-Encoding: Chunked
のチャンク転送データを、元のデータにパースするよ。
builder.response :chuncked
053 FaradayMiddleware::Caching - レスポンスをキャッシュする
🐱 レスポンスをキャッシュできるよ。キャッシュストアにはActiveSupport::Cache(Railsで使われているキャッシュ機能)
を利用できるよ。
# キャッシュの保存先をファイルにしたいので、FileStoreを使う store = ActiveSupport::Cache::FileStore.new("tmp") builder.response :caching, store
🐱 ignore_params
オプションを使うと、パラメータを無視して同一のキャッシュキーとして扱えるよ。
store = ActiveSupport::Cache::FileStore.new("tmp") # nameパラメータを無視してキャッシュする builder.response :caching, store, ignore_params: ["name"]
🐱 キャッシュストアは必要なメソッドを実装してればなんでもOKだよ。自作の参考 -> HTTP Request Response Caching Using Faraday: Part 1
054 FaradayMiddleware::FollowRedirects - リダイレクト先をGETする
🐱 リダイレクト先をGETするよ。
# http://facebook.com/aboutはhttps://www.facebook.com/facebookにリダイレクトするようになっているので、こちらのデータを取得できる。 connection = Faraday.new "http://facebook.com" do |builder| builder.response :follow_redirects builder.adapter :net_http end response = connection.get("/about")
🐱 第二引数でオプションを指定できるよ。
builder.response :follow_redirects, limit: 5
オプション一覧
オプション | 解説 | デフォルト値 |
---|---|---|
limit | リダイレクト回数 | 3 |
standards_compliant | HTTP標準仕様に準拠する | false |
callback | リダイレクト時のコールバック(procで指定) | nil |
🐱 クッキーを使いたい場合は、faraday-cookie_jar(別gem)も使うといいよ。
Faraday.new(url) do |builder| builder.response :follow_redirects builder.use :cookie_jar builder.adapter Faraday.default_adapter end
055 FaradayMiddleware::Gzip - レスポンスbodyをGzip解凍する
🐱 レスポンスbodyをGzip解凍するよ。Ruby1.9以上でnet_httpアダプタを使う場合は不要だよ。
056 FaradayMiddleware::Instrumentation - ActiveSupport::Notificationsを使い、リクエストを計測する
🐱 内部でActiveSupport::Notifications.instrument
を利用してイベントを発行しているよ。それをActiveSupport::Notifications.subscribe
を利用して、サブスクライブしてね。ActiveSupportのInstrumentation機能については、こちらのRailsガイドに詳しく載ってるよ。参考 -> Active Support の Instrumentation 機能
# コネクション生成 connection = Faraday.new("http://example.com") do |builder| builder.use :instrumentation builder.adapter Faraday.default_adapter end # サブスクライブ # キーは"request.faraday" ActiveSupport::Notifications.subscribe("request.faraday") do |name, start_time, end_time, _, env| puts end_time - start_time end # GET connection.get # リクエスト時間が出力される
057 faraday_middleware-parse_oj: JSONのパースにojを使う
🐱 faraday_middleware-parse_oj
はJSONのパースにoj
を使うgemだよ。oj
はRuby標準添付のjson
ライブラリよりも高速だと噂のJSONパーサーだよ。ojのリポジトリ
🐱 まずインストールしてね。
# Gemfile gem 'faraday' gem 'faraday_middleware-parse_oj'
$ bundle install
🐱 使い方は他のFaradayミドルウェアと同じだよ。
connection = Faraday.new do |builder| builder.response :oj builder.adapter Faraday.default_adapter end connection.get("http://example.com/some.json")
058 faraday-cookie_jar: クッキーを扱う
🐱 faraday-cookie_jar
はクッキーを扱うgemだよ。
🐱 まずインストールしてね。
# Gemfile gem 'faraday' gem 'faraday-cookie_jar'
$ bundle install
🐱 使い方は他のFaradayミドルウェアと同じだよ。
connection = Faraday.new("http://example.com") do |builder| builder.use :cookie_jar builder.adapter Faraday.default_adapter end connection.get "/one" # cookie取得 connection.get "/two" # cookie送信
059 faraday-detailed_logger: いい感じのログ
🐱 リクエスト/レスポンスの情報をいい感じに出力してくれるよ。curlっぽい出力になるよ。
🐱 まずはインストールしてね。
# Gemfile gem 'faraday' gem "faraday-detailed_logger"
$ bundle install
🐱 デフォルトでは標準出力にログを出力するよ。
connection = Faraday.new("https://google.com") do |builder| builder.response :detailed_logger builder.adapter Faraday.default_adapter end # このタイミングでログ出力 connection.get
出力内容
I, [2018-09-27T19:37:05.334495 #38411] INFO -- : GET https://google.com/ D, [2018-09-27T19:37:05.334773 #38411] DEBUG -- : "User-Agent: Faraday v0.15.2\n\n" I, [2018-09-27T19:37:05.646647 #38411] INFO -- : HTTP 301 D, [2018-09-27T19:37:05.646908 #38411] DEBUG -- : "location: https://www.google.com/\ncontent-type: text/html; charset=UTF-8\ndate: Thu, 27 Sep 2018 10:37:05 GMT\nexpires: Sat, 27 Oct 2018 10:37:05 GMT\ncache-control: public, max-age=2592000\nserver: gws\ncontent-length: 220\nx-xss-protection: 1; mode=block\nx-frame-options: SAMEORIGIN\nalt-svc: quic=\":443\"; ma=2592000; v=\"44,43,39,35\"\nconnection: close\n\n<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"https://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n"
🐱 カスタムロガーを使う場合は第二引数に指定してね。
logger = Logger.new("logfile.log") connection = Faraday.new("http://google.com") do |builder| builder.response :detailed_logger, logger builder.adapter Faraday.default_adapter end
🐱 Railsのloggerを使う場合はこんな感じになるよ。
builder.response :detailed_logger, Rails.logger
付録
Faradayの情報源
日本語の情報源
Web Clients for Ruby and What they should be in the future
🐱 RubyKaigi2016で@tkawaさんが行った、理想のWebクライアントについてのスライドと動画だよ。後半でWeb APIを個別のgemとして実装するのではなく、Faradayのミドルウェアとして実装する方法を紹介してるよ。
Webuilder240's Blog - FaradayでHTTPリクエストを並列で実行する方法
🐱 typhoeusアダプタを使った並列リクエストのコードとベンチマークが解説されてるよ。
ローファイ日記 - Faradayの話 - OpenStack クライアント開発日記 (3)
🐱 Faradayの自作ミドルウェアについて、詳しく解説されているよ。
Ruby の HTTP Client「Faraday」を使った場合の例外の扱いとリトライ処理をどうするか考えてみた
🐱 Faradayの例外処理とリトライ処理について、かなり詳しく考察されてるよ。
Developers.IO - Faraday の スタブテスト
🐱 FaradayのスタブとRSpecを利用したテストの方法が解説されてるよ。
Faradayを使ったプログラムをRspecでテスト(Railsでない)
🐱 こちらもFaradayのスタブとRSpecを利用したテストの方法が解説されてるよ。
Apitore blog - RubyでAPIコールするならFaradayが簡単便利
🐱 oauth2を使ったコードが解説されてるよ。
Sarabande.jp - Ruby: Faraday を使って HTTP リクエストを送信する
🐱 Faradayの基本的な使い方が解説されているよ。
成らぬは人の為さぬなりけり - Faradayを触ってみた
🐱 Faradayの基本的な使い方から、ミドルウェアの自作まで解説されているよ。
Ruby の HTTP クライアントライブラリ Faraday が便利そう
🐱 FaradayとFaraday Middlewareについて解説されてるよ。
英語の情報源
faradayのGithubリポジトリ
🐱 faradayのGithubリポジトリだよ。wikiにも詳しい解説が載ってるよ。
faraday_middlewareのGithubリポジトリ
🐱 faraday_middlewareのGithubリポジトリだよ。wikiにも詳しい解説が載ってるよ。
faraday_middleware-parse_ojのGithubリポジトリ
🐱 faraday_middleware-parse_ojのGithubリポジトリだよ。
miyagawa/faraday-cookie_jarのGithubリポジトリ
🐱 faraday-cookie_jarのGithubリポジトリだよ。
faraday-detailed_loggerのGithubリポジトリ
🐱 faraday-detailed_loggerのGithubリポジトリだよ。
HTTP Request Response Caching Using Faraday: Part 1
🐱 FaradayMiddleware::Cachingで自作のキャッシュストアを利用する方法を解説しているよ。
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版
- この商品を含むブログを見る