猫Rails

ねこー🐈

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::CollectionDecoratordelegate

# config/initializers/draper.rb
Draper::CollectionDecorator.delegate :current_page, :total_pages, :limit_value, :total_count

解決方法2: Draper::CollectionDecoratorのサブクラスをコレクションにする

# 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をレンダリング

# デコレータ
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

デコレータはモデルのように振る舞う

@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.localizelで使える
  • 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アプリ作るならpuma使えば良さそう。でも、unicornやpassengerから乗り換えるほどではないかも(ケースバイケースだがパフォーマンスの顕著な差はないっぽい)
  • Heroku使う場合はメモリ使用量が少なくて良さそう。メモリ500MBプランだとunicornで2worker動かすの辛かった記憶

スレッドベース

参考

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

設定ファイルの読み込み

  • 3つの起動コマンド(rails spumactl startpuma)は、以下の設定ファイルを自動で読み込む
    • 環境指定がない場合: 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{}: 各ワーカーのフォーク前の処理

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: リクエストをキューする

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向けの設定を用意してくれる

自分で実装

  • 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

puma + Nginx

puma + heroku

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コマンドを使う場合
$ 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の使い方 まとめ

感想とか

注意点

  • これは自分用のまとめを公開したものです。ドキュメント/ソースコードを見ただけで試していないコードも多いので、参考程度に。

感想

  • 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が結構変わったので注意

参考サイト

https://github.com/rest-client/rest-client

Faradayの使い方 59のレシピ

第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_timeouttimeoutで指定するよ。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::Middlewareon_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")

🐱 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リポジトリだよ。

🐱 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年の頼りになる先輩。猫だからかわいい。

プロローグ

👦🏻 むーん…

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

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

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

👦🏻 らんさっく?

🐱 Ransackって言うのはね…

環境

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

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

第1章 Ransackをはじめよう

001 Ransackとは?

👦🏻 Ransackってなにー?

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

👦🏻 ほえーん。

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

002 セットアップ

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

gem 'ransack'

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

$ bundle install

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

003 使ってみよう

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

コントローラー

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

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

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

ビュー

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

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

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

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

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

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

👦🏻 ほいーん。

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

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

004 シンプルモードとは?

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

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

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

005 述語とは?

👦🏻 述語(Predicate)って何?

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

006 eq - =検索

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

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

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

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

f.text_field :name_eq

007 matches - LIKE検索

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

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

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

f.text_field :name_matches

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

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

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

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

f.text_field :name_cont

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

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

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

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

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

f.text_field :name_start

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

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

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

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

f.text_field :name_end

011 gt - >検索

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

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

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

f.text_field :price_gt

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

012 gteq - >=検索

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

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

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

f.text_field :price_gteq

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

013 lt - <検索

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

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

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

f.text_field :price_lt

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

014 lteq - <=検索

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

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

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

f.text_field :price_lteq

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

015 true - trueの検索

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

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

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

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

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

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

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

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

016 false - falseの検索

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

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

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

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

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

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

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

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

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

017 blank - blank?の検索

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

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

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

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

018 present - present?の検索

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

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

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

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

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

019 null - NULLの検索

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

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

020 in - IN検索

🐱 ArrayとRangeが使えるよ。

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

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

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

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

021 not - 否定

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

🐱 allOR版だね。

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

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

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

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

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

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

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

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

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

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

026 関連

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

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

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

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

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

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

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

027 条件を組み合わせる

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

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

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

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

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

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

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

028 範囲検索

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

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

🐱 ビューはこんなんー。

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

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

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

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

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

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

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

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

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

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

# config/initializers/ransack.rb

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<% end %>

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

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

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

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

7つの要素

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

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

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

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

searchをRailsコンソールで覗く

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ちなみに

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

034 c(conditions) - 条件

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

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

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

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

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

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

作ってみよう

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

f:id:nekorails:20170530000504p:plain

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

# index.html.erb

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

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

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

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

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

  <% end %>

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

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

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

# products_controller.rb

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

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

f:id:nekorails:20170530000508p:plain

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

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

conditionを2つ用意する

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

# products_controller.rb

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

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

f:id:nekorails:20170530000511p:plain

condition関係のメソッド

grouping.conditions - conditionsを取得する

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

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

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

grouping.build_condition - conditionを作成する

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

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

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

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

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

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

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

<% end %>

035 a(attributes) - 属性

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

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

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

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

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

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

attributeを固定する

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

# index.html.erb

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

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

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

    <%= c.predicate_select %>

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

  <% end %>

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


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

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

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

f:id:nekorails:20170530000515p:plain

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

# index.html.erb

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

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

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

    <%= c.predicate_select %>

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

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

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

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

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

# users_controller.rb

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

  @users = @search.result
end

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

f:id:nekorails:20170530000520p:plain

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

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

attribute関係のメソッド

condition.attributes - attributesを取得

condition.attributes
=> [Attribute <first_name>]

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

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

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

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

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

    # ...略...

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

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

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

f:id:nekorails:20170530000527p:plain

036 p(predicate) - 述語

predicateを固定する

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

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

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

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

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

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


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




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

predicate関係のメソッド

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

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

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

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

037 v(values) - 値

複数ある場合

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

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

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

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

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

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

predicate関係のメソッド

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

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

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

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


    # ...略...

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

基本的な使い方

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

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

f:id:nekorails:20170530000531p:plain

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

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

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

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

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

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

      <%= c.predicate_select %>

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

    <% end %>

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

    <br/>
  <% end %>

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

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


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

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

# users_controller.rb

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

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

  @users = @search.result
end

🐱 これで完成だよ。

groupingを入れ子にする

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

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

f:id:nekorails:20170530000536p:plain

🐱 SQLはこんな感じだよ。

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

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

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

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

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

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

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

        <%= c.predicate_select %>

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

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

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

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

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

  @users = @search.result
end

groupings関係のメソッド

grouping.groupings - groupingsを取得する

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

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

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

grouping.build_grouping - groupingを作成する

grouping.build_grouping
=> Grouping <>

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

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

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

039 m(combinator) - 論理演算子

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

conditionsに使う

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

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

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

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

groupingsに使う

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

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

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

combinator関係のメソッド

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

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

040 s(sorts) - ソート

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

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

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

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

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

f:id:nekorails:20170530000539p:plain

🐱 SQLはこんな感じだよ。

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

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

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

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

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

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

  @users = @search.result
end

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

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

f:id:nekorails:20170530000543p:plain

🐱 SQLはこんな感じだよ。

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

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

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

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

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

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

  @users = @search.result
end

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

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

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

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

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

f:id:nekorails:20170530000546p:plain

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

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

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

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

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

f:id:nekorails:20170530000550p:plain

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

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

  @users = @search.result
end

f:id:nekorails:20170530000554p:plain

複数指定する

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

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

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

f:id:nekorails:20170530000557p:plain

sort関係のメソッド

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

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

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

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

search.sorts - sortを取得する

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

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

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

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

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

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

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

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

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

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

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

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

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

第4章 scopeで検索する

041 scopeで検索する

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

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

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

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

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

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

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

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

f.check_box :activated

042 booleanへの変換に注意する

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

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

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

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

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

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

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

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

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

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

043 カスタム述語とは?

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

# config/initializers/ransack.rb

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

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

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

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

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

👦🏻 まぁね。

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

👦🏻 ええやん。

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

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

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

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

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

f.search_field :created_at_lteq_end_of_day

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

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

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

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

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

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

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

f.search_field :name_has_every_term

第6章 ransackerで検索する

046 ransakerとは?

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

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

ransacker name, options

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

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

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

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

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

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

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

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

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

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

f.search_field :created_at_eq

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

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

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

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

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

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

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

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

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

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

f.search_field :id_cont

049 full_nameを検索する

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

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

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

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

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

f.search_field :full_name_eq

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

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

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

f.search_field :full_name_cont

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

左辺を反転して検索する

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

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

🐱 SQLはこんな感じだよ。

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

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

f.search_field :reversed_name_eq

右辺を反転して検索する

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

# product.rb

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

🐱 SQLはこんな感じだよ。

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

051 ransackerのコツ

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

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

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

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

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

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

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

仮想属性に別名をつける

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

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

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

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

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

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

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

第7章 4つの認可

052 認可とは?

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

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

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

# user.rb

class User < ApplicationRecord
  private

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

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

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

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

f:id:nekorails:20170530000601p:plain)

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

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

# user.rb

class User < ApplicationRecord
  private

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

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

  private

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

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

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

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

第8章 設定

054 Ransackを設定する

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

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

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

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

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

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

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

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

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

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

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

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

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

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

第9章 RansackのためのArel入門

060 Arelとは?

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

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

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

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

061 Arelの基本

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

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

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

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

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


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

062 ArelとActiveRecord

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

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

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

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

063 ArelとRansack

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

  • カスタム述語
  • ransacker

カスタム述語とArel

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

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

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

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

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

ransackerとArel

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

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

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

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

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

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

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

第10章 Tips詰め合わせ

064 重複を取り除く

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

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

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

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

# users_controller.rb

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

  @users = @search.result
end

066 常にOR検索する

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

# users_controller.rb

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

  @users = @search.result
end

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

# users_controller.rb

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

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

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

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

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

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

068 searchメソッド

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

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

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

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

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

# config/initializers/ransack.rb:

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

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

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

class Post < ApplicationRecord
  belongs_to :author

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

070 Ransackの日本語化

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

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

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

071 searchアクションにPOSTする

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

# routes.rb

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

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

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

072 Kaminariと一緒に使う

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

# users_controller.rb

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

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

# users_controller.rb

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

073 Mongoidと一緒に使う

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

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

付録

Ransackの情報源

日本語の情報源

RailsCasts - Ransack

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

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

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

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

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

Qiita - Ransackのススメ

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

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

英語の情報源

readme

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

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

githubのwiki

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

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

Ransackのデモサイト

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

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

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

Ransackのソースコード

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

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

LocalApp - Ransack

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

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

Kindle Unlimited版の紹介

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