Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsのrespond_toを苦痛なく使いこなす方法

Railsでスキャフォールドを生成すると、おなじみのrespond_toブロックが自動的に作成されます。

app/controllers/tasks_controller.rb
  def destroy
    @task.destroy
    respond_to do |format|
      format.html { redirect_to tasks_url, notice: 'Task was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

しかし、よく見るとindexなど一部のアクションには、このブロックが存在していません。

app/controllers/tasks_controller.rb
  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
  end

これは問題です。なぜなら、アプリがサポートしていない形式(たとえば/tasks.txt)にアクセスされると、まったく見当違いのエラーが発生してしまうからです。

ActionView::MissingTemplate (Missing template tasks/index, application/index with {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}

これは正しい挙動ではありません。クライアントに対しては「リクエストされた形式はサポートしていません」と伝えるべきであり、「該当するファイルが見つかりません」と返すのは誤りです。

もしUnknownFormat例外として検出できれば、より適切なレスポンスコードを返せます。現状では、この種のエラーが他の無関係なエラーと混ざってしまい、個別にハンドリングするのが非常に困難になります。

indexアクションにrespond_toブロックを追加すれば解決できます。

app/controllers/tasks_controller.rb
  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
    respond_to do |format|
      format.html
      format.json
    end
  end

すると、期待どおりの例外とステータスコードが得られます。

Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:05:12 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 21ms

ActionController::UnknownFormat (ActionController::UnknownFormat):
  app/controllers/tasks_controller.rb:8:in `index'

ずっと良くなりました。しかしすべてのコントローラにrespond_toブロックを書き散らすのは現実的ではありません。Railsらしさに欠け、DRY原則にも反しますし、コントローラ本来の仕事への集中も妨げてしまいます。

とはいえ、不正なフォーマットへのリクエストは適切に処理したいもの。どうすればよいのでしょうか?

respond_toのショートカット構文

オブジェクトの描画に特別な処理が必要ないのであれば、ショートカットが使えます。次のように書くと:

app/controllers/tasks_controller.rb
def index
  @tasks = Task.all
  respond_to :html, :json
end

indexに完全なrespond_toブロックを書いた場合と同じ挙動になります。これは「アクションが対応しているフォーマットの一覧」をRailsに簡潔に伝えるための記法です。アクションごとにサポートするフォーマットが異なる場合も、少ないコード量でその違いをきれいに表現できます。

コントローラレベルでフォーマットを管理する

しかし通常、コントローラ内の各アクションは同じフォーマット群を扱うことがほとんどでしょう。indexがJSONに応答するなら、newcreateなど他のアクションも同様に対応しているはずです。そこで、コントローラ全体に作用するrespond_toが書ければ便利です:

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  respond_to :html, :json

  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
    respond_with(@tasks)
  end

実際にこれで動作します。

Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:17:37 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 7ms

ActionController::UnknownFormat (ActionController::UnknownFormat):
  app/controllers/tasks_controller.rb:8:in `index'

まさに期待していた通りのエラーです。しかも各アクションを一つひとつ修正する必要はありませんでした。

モデルの状態によって挙動を変えたいケースもあります。たとえばcreateアクションでは、モデルが有効かどうかに応じて、リダイレクトするかフォームを再描画するかを切り替えますよね。

Railsはこれにも対応しています。ただし、respond_withで「どのオブジェクトを判定対象にするか」を教えてあげる必要があります。つまり、こう書いていたものが:

app/controllers/tasks_controller.rb
  def create
    @task = Task.new(task_params)

    respond_to do |format|
      if @task.save
        format.html { redirect_to @task, notice: 'Task was successfully created.' }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :new }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

こう書けるようになります。

app/controllers/tasks_controller.rb
  def create
    @task = Task.new(task_params)
    flash[:notice] = "Task was successfully created." if @task.save
    respond_with(@task)
  end

これにより、ビジネスロジックと「応答するフォーマットの定義」が分離されます。対応フォーマットは一度だけ宣言すればよく、アクションごとに繰り返す必要はありません。

responders gemの活用

Rails 4.2以降には注意点があります。respond_withは本体から除外されました。しかしresponders gemをインストールすれば、この機能を取り戻せます。さらに、このgemには他にも便利な機能が用意されています。

コントローラの先頭でresponders :flashと宣言すると、respond_with経由でフラッシュメッセージを設定できるようになります。

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  responders :flash

さらに便利なことに、これらのフラッシュメッセージはロケールファイル(yml)側でデフォルト値を定義しておけます。

また、Gemfileresponders gemが含まれた状態でスキャフォールドを生成すると、ジェネレータはrespond_toではなくrespond_withを使ったコントローラを出力します。

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  respond_to :html, :json
  
  def index
    @tasks = Task.all
    respond_with(@tasks)
  end

  def show
    respond_with(@task)
  end

  # ...

Rails標準のスキャフォールドよりも、かなりすっきりしたコードになります。

最後に、特定のアクションだけ追加のフォーマットに応答させたい場合は、respond_toを複数回呼び出すことも可能です。

class TasksController < ApplicationController
  respond_to :html
  respond_to :js, only: :create
end

最後のTipsは、コメント欄にてJeroen Weeinkさんにご教授いただきました。ありがとうございます!

respond_withとrespond_to、どちらを選ぶべきか?

フォーマットごとに異なる情報を返したい場合、いくつかの選択肢があります。コントローラレベルのrespond_torespond_withの組み合わせは、コントローラを短く保つための優れた手段です。ただしその効果を最大限に発揮できるのは、すべてのアクションが同じフォーマットに対応し、Railsが想定する標準的な振る舞いをする場合です。

一方、一部のアクションだけ特殊な動作をさせたい場面もあります。そんなときは、1行で書けるショートカット版のrespond_toが便利です。

さらに細かい制御が必要であれば、ブロック付きの完全なrespond_toを使えば、フォーマットごとの処理を自由自在に記述できます。

どの方法を選んでも、未サポートのフォーマットへのリクエストには正しいエラーが返されるようになります。その結果、アプリケーションもクライアント側も、混乱から大きく解放されるはずです。

  1. MongoDBコレクションの全キー(フィールド名)を取得する方法まとめ

    この記事は2019年1月18日にObjectRocket.com/blogで公開された内容をもとにしています。 MongoDB®のコレクションに含まれるすべてのキーを把握することは、スキーマの検証、フィールド名のタイプミスのデバッグ、設定すべきでないフィールドの発見など、さまざまな場面で役立ちます。本記事では、MongoDBコレクション内の全キー名を取得する複数の方法を、状況別にわかりやすく解説します。 まず検討したい:ODMやマネージドサービスの活用 ObjectRocketをはじめとする多くのMongoDB-as-a-Service事業者は、ユーザーインターフェース(UI)上で簡単にキーを

  2. MongoDBコレクション内の全キー名を取得する方法|MapReduce・Aggregation・シェルなど状況別に解説

    スキーマの検証、フィールド名のタイプミスのデバッグ、設定されるべきではないフィールドの発見などを行うには、まずMongoDBコレクション内にどのようなキーが存在するのかを把握しておく必要があります。多くのMongoDB-as-a-Service事業者は、UI上で手軽にこれを実現できる機能を提供しています(ObjectRocketもそのひとつです)。また、経験豊富なMongoDBユーザーの多くは、JavaScript向けのMongooseやPython向けのMongoengineといったオブジェクトドキュメントマッパー(ODM)を導入しています。ODMを使えば、アプリケーション向けの一貫性のある