Action Controller の概要

本ガイドでは、コントローラの動作と、アプリケーションのリクエストサイクルにおけるコントローラの役割について解説します。

このガイドの内容:

  • コントローラを経由するリクエストの流れを理解する
  • コントローラに渡されたパラメータにアクセスする方法
  • Strong Parametersで値を許可する方法
  • データをcookie、セッション、フラッシュに保存する方法
  • リクエストの処理中にアクションコールバックでコードを実行する方法
  • requestオブジェクトとresponseオブジェクトの使い方

1 はじめに

Action Controllerは、MVCアーキテクチャの「C」に相当します。リクエストを処理するコントローラがルーターによって決定されると、コントローラはリクエストの意味を理解して適切な出力を行う役目を担います。ありがたいことに、これらの処理のほとんどはAction Controllerが行ってくれます。リクエストは、十分に吟味された規約によって可能な限りわかりやすい形で処理されます。

伝統的なRESTfulアプリケーションでは、コントローラ(C)がリクエストの受信を担当し、モデル(M)がデータの取得や保存を担当し、ビュー(V)がHTML出力を担当します。

つまり、コントローラは「モデルとビューの間を仲介する」と考えられます。コントローラがモデルのデータをビューで利用可能にすることで、データをビューで表示したり、入力されたデータでモデルを更新したりします。

2 コントローラを作成する

Railsのコントローラは、ApplicationControllerを継承したRubyのクラスであり、他のクラスと同様にメソッドが使えます。アプリケーションがブラウザからのリクエストを受け取ると、ルーターによってコントローラとアクションが確定し、Railsはそれに応じてコントローラのインスタンスを生成し、アクション名と同じ名前のメソッドを実行します。

class ClientsController < ApplicationController
  def new
  end
end

上のClientsControllerは、ユーザーがブラウザでアプリケーションの/clients/newにアクセスして新しいクライアントを追加すると、ClientsControllerのインスタンスが作成され、そのインスタンスのnewメソッドが呼び出されます。

newメソッドが存在しているが中身が空の場合、Railsはデフォルトでnew.html.erbビューを自動的にレンダリングします。

このnewメソッドはインスタンスメソッドなので、ClientsControllerのインスタンスで呼び出されます(つまりインスタンスを作らないと呼び出せません)。newメソッドのように、インスタンスを作らずにClientsController.newで呼び出せるクラスメソッドと混同しないようにしましょう。

newクラスメソッドを実行したときの典型的な振る舞いは次の通りです。コントローラがClientモデルのインスタンスを作成し、それをビューで@clientというインスタンス変数として利用できるようにします。

def new
  @client = Client.new
end

ApplicationControllerを継承したすべてのコントローラは、最終的にActionController::Baseを継承します。ただし、API専用アプリケーションの場合のみ、ApplicationControllerActionController::APIを継承します。

3 コントローラの命名規約

Railsのコントローラ名には、基本的に英語の「複数形」を使うのが望ましい命名です(ただし末尾の「Controller」という語は固定です)。たとえば、ClientsControllerの方がClientControllerより好ましく、SiteAdminsControllerの方がSiteAdminControllerSitesAdminsControllerよりも好ましいといった具合です。 なお、この規約は絶対ではありません(実際ApplicationControllerはApplicationが単数形です)。

しかし、この規約は守っておくことをおすすめします。規約を守ることで、:controllerオプションをわざわざ書かなくても、resourcesなどのデフォルトのルーティングジェネレーターをそのまま利用できるようになりますし、生成される名前付きルーティングヘルパー名もアプリケーション全体で一貫するからです。

コントローラの命名規約はモデルの命名規約と異なることにご注意ください。コントローラ名は「複数形」が望ましい命名ですが、モデル名は「単数形」が望ましい命名です。

コントローラのアクションは、アクションとして呼び出し可能なpublicメソッドでなければなりません。ヘルパーメソッドのような「アクションとして外部から呼び出したくない」メソッドには、privateprotectedを指定して公開しないようにするのが定石です。

ある種のメソッド名はAction Controllerで予約されているため、利用できません。予約済みメソッドを誤ってアクションやヘルパーメソッドとして再定義すると、SystemStackErrorが発生する可能性があります。コントローラ内でRESTfulなリソースルーティングアクションだけを使うようにしていれば、メソッド名が使えなくなる心配はありません。

予約済みメソッド名をアクション名として使わざるを得ない場合は、たとえばカスタムルーティングを利用して、予約済みメソッド名を予約されていないアクションメソッド名に対応付けるという回避策が考えられます。

4 パラメータ

リクエストによって送信されたデータを受信すると、コントローラ内ではparamsハッシュとして利用できます。パラメータのデータには以下の2種類があります:

  • URL の一部として送信されるクエリ文字列パラメータ (例: http://example.com/accounts?filter=free?以降のfilter=freeの部分)
  • HTMLフォームから送信されるPOSTパラメータ

Railsは、クエリ文字列パラメータとPOSTパラメータを区別しません。以下のように、どちらもコントローラのparamsハッシュで同じように利用できます。

class ClientsController < ApplicationController
  # このアクションは、"/clients?status=activated"というURLへの
  # HTTP GETリクエストからクエリ文字列パラメータを受け取る
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # このアクションは、"/clients"というURLへのHTTP POSTリクエストに含まれる
  # リクエストbody内のフォームデータからパラメータを受け取る
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      render "new"
    end
  end
end

paramsハッシュは、Rubyの単なるHashではなく、ActionController::Parametersオブジェクトである点にご注意ください。このオブジェクトはRubyのHashのように振る舞いますが、Hashを継承していません。また、paramsをフィルタリングするためのメソッドが提供され、シンボルキー:fooと文字列キー"foo"が同じものと見なされる点もHashと異なります。

4.1 ハッシュと配列のパラメータ

paramsハッシュには、一次元のキーバリューペアの他に、ネストした配列やハッシュも保存できます。値の配列をフォームから送信するには、以下のようにキー名に空の角かっこ[]のペアを追加します。

GET /users?ids[]=1&ids[]=2&ids[]=3

[]はURLで利用できない文字なので、この例の実際のURLは/users?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3のようになります。これについては、ブラウザで自動的にエンコードされ、Railsがパラメータを受け取るときに自動的に復元するので、開発者が気にする必要はほとんどありません。ただし、何らかの理由でサーバーにリクエストを手動送信しなければならない場合には、このことを思い出す必要があるでしょう。

これで、受け取ったparams[:ids]の値は["1", "2", "3"]になりました。ここで重要なのは、パラメータの値は常に「文字列」になることです。Railsはパラメータの型推測や型変換を行いません。

paramsの中にある[nil][nil, nil, ...]などの値は、セキュリティ上の理由でデフォルトでは[]に置き換えられます。詳しくはセキュリティガイドを参照してください。

フォームからハッシュを送信するには、以下のようにキー名を角かっこ[]の中に置きます。

<form accept-charset="UTF-8" action="/users" method="post">
  <input type="text" name="user[name]" value="Acme" />
  <input type="text" name="user[phone]" value="12345" />
  <input type="text" name="user[address][postcode]" value="12345" />
  <input type="text" name="user[address][city]" value="Carrot City" />
</form>

このフォームを送信すると、params[:user]の値は以下のようになります。params[:user][:address]のハッシュがネストしていることにご注目ください。

{ "name" => "Acme",
  "phone" => "12345",
  "address" => {
    "postcode" => "12345",
    "city" => "Carrot City"
  }
}

このparamsオブジェクトの振る舞いはRubyのHashと似ていますが、キー名にシンボルと文字列のどちらでも指定できる点がHashと異なります。

4.2 複合主キーのパラメータ

複合キーパラメータは、1個のパラメータに複数の値を含み、値同士は区切り文字(アンダースコアなど)で区切られます。したがって、Active Recordに渡すには個別の値を抽出する必要があります。これを行うには、extract_valueメソッドを使います。

たとえば以下のコントローラがあるとします。

class BooksController < ApplicationController
  def show
    # URLパラメータから複合ID値を抽出する
    id = params.extract_value(:id)
    @book = Book.find(id)
  end
end

ルーティングは以下のようになっているとします。

get "/books/:id", to: "books#show"

ユーザーが/books/4_2というURLでリクエストを送信すると、コントローラは複合キー値["4", "2"]を抽出してからBook.findに渡します。このようにextract_valueメソッドを使うことで、区切り文字で区切られたパラメータから配列を抽出できます。

4.3 JSONパラメータ

アプリケーションでAPIを公開している場合、パラメータをJSON形式で受け取ることになるでしょう。リクエストのContent-Typeヘッダーがapplication/jsonに設定されていると、Railsは自動的にパラメータをparamsハッシュに読み込んで、通常と同じようにアクセスできるようになります。

たとえば、以下のJSONコンテンツを送信したとします。

{ "user": { "name": "acme", "address": "123 Carrot Street" } }

このとき、コントローラは以下を受け取ります。

{ "user" => { "name" => "acme", "address" => "123 Carrot Street" } }
4.3.1 wrap_parametersを設定する

wrap_parametersメソッドを使うと、コントローラ名がJSONパラメータに自動で追加されます。たとえば、以下のJSONは:userというrootキープレフィックスなしで送信できます。

{ "name": "acme", "address": "123 Carrot Street" }

上のデータをUsersControllerに送信すると、以下のように:userキー内にラップされたJSONデータも追加されます。

{ name: "acme", address: "123 Carrot Street", user: { name: "acme", address: "123 Carrot Street" } }

wrap_parametersは、コントローラ名と同じキー内のハッシュにパラメータの複製を追加します。その結果、paramsハッシュには、パラメータの元のバージョンと「ラップされた」バージョンのパラメータの両方が存在することになります。

この機能は、パラメータを複製してから、コントローラ名に基づいて選択したキーを用いてラップします。これを制御するconfig.action_controller.wrap_parameters_by_default設定はデフォルトでtrueに設定されていますが、パラメータをラップしたくない場合は以下のようにfalseに設定できます。

config.action_controller.wrap_parameters_by_default = false

キー名のカスタマイズ方法や、ラップしたいパラメータを指定する方法について詳しくは、ActionController::ParamsWrapper APIドキュメントを参照してください。

訳注: 従来のXMLパラメータ解析のサポートは、Rails 4.0のときにactionpack-xml_parserというgemに切り出されました。

4.4 ルーティングパラメータ

パラメータをroutes.rbファイル内のルーティング宣言の一部として指定すると、そのパラメータをparamsハッシュでも利用可能になります。たとえば、クライアントの:statusパラメータを取得するルーティングは以下のように追加できます。

get "/clients/:status", to: "clients#index", foo: "bar"

この場合、ブラウザで/clients/activeというURLを開くと、params[:status]"active"(有効)に設定されます。このルーティングを使うと、クエリ文字列で渡したのと同様にparams[:foo]にも"bar"が設定されます。

ルーティング宣言で定義された他のパラメータ(:id など)にも同様にアクセスできます。

上の例では、コントローラはparams[:action]"index"として、params[:controller]"clients"として受け取ります。paramsハッシュには常に:controllerキーと:actionキーが含まれますが、これらの値にアクセスする場合は、params[:controller]params[:action]のような方法ではなく、controller_nameメソッドやaction_nameメソッドを使うことが推奨されます。

4.5 default_url_optionsメソッド

コントローラで以下のようにdefault_url_optionsという名前のメソッドを定義すると、url_forヘルパーのグローバルなデフォルトパラメータを設定できます。

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

このメソッドで指定したデフォルトパラメータは、URLを生成する際の開始点として使われるようになります。これらのデフォルトパラメータは、url_forに渡したオプションや、posts_pathなどのパスヘルパーで上書きできます。たとえば、locale: I18n.localeを設定すると、Railsは以下のようにすべてのURLにロケールを自動的に追加します。

posts_path # => "/posts?locale=en"

このパラメータは、必要に応じて以下のように上書きできます。

posts_path(locale: :fr) # => "/posts?locale=fr"

posts_pathヘルパーは、内部的にはurl_forを適切なパラメータで呼び出すためのショートハンドです。

ApplicationControllerで上の例のようにdefault_url_optionsを定義すると、これらのデフォルトパラメータがすべてのURL生成で使われるようになります。このdefault_url_optionsメソッドは特定のコントローラで定義することも可能で、その場合は、そのコントローラ用に生成されたURLにのみ適用されます。

リクエストを受け取ったときに、生成されたURLごとにこのメソッドが常に呼び出されるとは限りません。パフォーマンス上の理由から、返されたハッシュはリクエストごとにキャッシュされます。

5 Strong Parameters

Action ControllerのStrongParametersは、明示的に許可されていないパラメータをActive Modelの「マスアサインメント(mass-assignment: 一括代入)」で利用することを禁止します。したがって、開発者は、どの属性でマスアップデート(mass-update: 一括更新)を許可するかをコントローラで必ず明示的に宣言しなければなりません。strong parametersは、ユーザーがモデルの重要な属性を誤って更新してしまうことを防止するためのセキュリティ対策です。

さらに、strong parametersではパラメータを必須(つまり省略不可)として指定できます。リクエストで渡された必須パラメータが不足している場合は、400 Bad Requestを返します。

class PeopleController < ActionController::Base
  # 以下のコードはActiveModel::ForbiddenAttributesError例外を発生する
  # (明示的に許可していないパラメータを一括で渡してしまう危険な「マスアサインメント」が行われているため)
  def create
    Person.create(params[:person])
  end

  # 以下のコードは`person_params`ヘルパーメソッドを使っているため正常に動作する。
  # このヘルパーメソッドには、マスアサインメントを許可する`expect`呼び出しが含まれている。
  def update
    person = Person.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # 許可するパラメータは、このようにprivateメソッドでカプセル化するのがよい手法である。
    # このヘルパーメソッドは、createとupdateの両方で同じ許可リストを与えるのに使える。
    def person_params
      params.expect(person: [:name, :age])
    end
end

5.1 値を許可する

5.1.1 expect

Rails 8から導入されたexpectメソッドは、パラメータの必須化とパラメータの許可を同時に行うための簡潔かつ安全な方法を提供します。

id = params.expect(:id)

上のexpectは常にスカラー値を返すようになり、配列やハッシュを返すことはありません。

expectの別の利用例としてはフォームパラメータがあります。以下のようにexpectを使うことで、rootキーが存在することと、属性が許可されていることを保証できます。

user_params = params.expect(user: [:username, :password])
user_params.has_key?(:username) # => true

上の例では、:userキーが指定のキー(:username:password)を持つ「ネストしたハッシュ」でない場合、expectはエラーを発生して「400 Bad Request」レスポンスを返します。

パラメータのハッシュ全体に対して(つまりハッシュの内容を問わずに)必須化と許可を同時に行いたい場合は、expectで以下のように空ハッシュ{}を指定することも「一応」可能です。

params.expect(log_entry: {})

この場合、:log_entryパラメータハッシュとそのサブハッシュはすべて許可済みとして扱われ、スカラー値が許可済みかどうかのチェックも行われなくなるため、どんなサブハッシュを渡してもすべて受け入れるようになります。

このように空ハッシュ{}を渡してexpectを呼び出すと、現在のモデル属性だけでなく、今後追加されるすべてモデル属性も無条件にマスアサインメントされる可能性があるため、取り扱いには細心の注意が必要です。

5.1.2 permit

permitを呼び出すと、params内の指定したキー(以下の例では:idまたは:admin)をcreateアクションupdateアクションなどのマスアサインメントに含めることを許可できます。

params = ActionController::Parameters.new(id: 1, admin: "true")
#=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: false>

params.permit(:id)
#=> #<ActionController::Parameters {"id"=>1} permitted: true>

params.permit(:id, :admin)
#=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: true>

上で許可した:idキーの値は、以下の許可済みスカラー値のいずれかでなければなりません。

  • String
  • Symbol
  • NilClass
  • Numeric
  • TrueClass
  • FalseClass
  • Date
  • Time
  • DateTime
  • StringIO
  • IO
  • ActionDispatch::Http::UploadedFile
  • Rack::Test::UploadedFile

permitを呼び出さなかったキーは、フィルタで除外されます(訳注: エラーは発生しません)。配列やハッシュ、およびその他のオブジェクトは、デフォルトでは挿入されません。

許可済みのスカラー値を要素に持つ配列をparamsの値に含めることを許可するには、以下のようにキーに空配列[]を対応付けます。

params = ActionController::Parameters.new(tags: ["rails", "parameters"])
#=> #<ActionController::Parameters {"tags"=>["rails", "parameters"]} permitted: false>

params.permit(tags: [])
#=> #<ActionController::Parameters {"tags"=>["rails", "parameters"]} permitted: true>

ハッシュ値をparamsの値に含めることを許可するには、以下のようにキーに空ハッシュ{}を対応付けます。

params = ActionController::Parameters.new(options: { darkmode: true })
#=> #<ActionController::Parameters {"options"=>{"darkmode"=>true}} permitted: false>

params.permit(options: {})
#=> #<ActionController::Parameters {"options"=>#<ActionController::Parameters {"darkmode"=>true} permitted: true>} permitted: true>

上のpermit呼び出しは、options内の値が許可済みのスカラー値であることを保証し、それ以外のものをフィルタで除外します。

ハッシュパラメータや、その内部構造の有効なキーをいちいち宣言することが不可能な場合や不便な場合があるため、permitに空ハッシュ{}を渡せるのは確かに便利ではあります。ただし、上のようにpermitで空ハッシュ{}を指定すると、ユーザーがどんなデータでもパラメータとして渡せるようになってしまうことは認識しておかなければなりません。

5.1.3 permit!

値をチェックせずにパラメータのハッシュ全体を許可する!付きのpermit!メソッドも利用可能です。

params = ActionController::Parameters.new(id: 1, admin: "true")
#=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: false>

params.permit!
#=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: true>

permit!を使うと、現在のモデル属性だけでなく、今後追加されるすべてのモデル属性も無条件にマスアサインメントされる可能性があるため、取り扱いには細心の注意が必要です。

5.1.4 ネストしたパラメータを許可する

expect(またはpermit)は、以下のようにネストしたパラメータ(ネステッドパラメータ)に対しても使えます。

# 期待されるパラメータの例:
params = ActionController::Parameters.new(
  name: "Martin",
  emails: ["me@example.com"],
  friends: [
    { name: "André", family: { name: "RubyGems" }, hobbies: ["keyboards", "card games"] },
    { name: "Kewe", family: { name: "Baroness" }, hobbies: ["video games"] },
  ]
)

# パラメータは以下のexpectによって許可済みであることが保証される:
name, emails, friends = params.expect(
  :name,                 # 許可済みのスカラー値
  emails: [],            # 許可済みのスカラー値の配列
  friends: [[            # 許可済みのParameterハッシュの配列
    :name,               # 許可済みのスカラー値
    family: [:name],     # family: { name: "許可済みのスカラー値" }
    hobbies: []          # 許可済みのスカラー値の配列
  ]]
)

この宣言は、name属性、emails属性、friends属性を許可し、それぞれ以下を返すことが前提となっています。

  • emails属性: 許可済みのスカラー値を要素に持つ配列を返す
  • friendsは特定の属性を持つリソースの配列を返す (配列を明示的に必須にするための新しい二重配列構文[[ ]]が使われていることに注意) このリソースには以下の属性が必要:
    • name属性: 許可済みの任意のスカラー値
    • hobbies属性: 許可済みのスカラー値を要素に持つ配列
    • family属性: nameキーと、任意の許可済みスカラー値を持つハッシュのみに制限される

訳注: この特殊な二重配列構文[[:属性名]]は、Rails 8.0に導入された新しい配列マッチング構文です(#51674)。[[:属性名]]は、(ハッシュではなく)配列を渡さなければならないことを意味します。ただし、従来からあるpermitでは、後方互換性のため、[[:属性名]]が指定されている場合にハッシュを渡しても許容されますが、新しいexpectでは、[[:属性名]]に配列以外のものを渡すとエラーになります。

5.2 Strong Parametersの例

permitexpectの使い方の例をいくつか紹介します。

例1: newアクションで許可済み属性を使いたい場合の利用法です(newを呼び出した時点ではrootキーが存在しないのが普通なので、そのままではrootキーに対してrequireを呼び出せません)。

# この場合は以下のように`fetch`を使うことで、デフォルトを指定しつつ
# Strong Parameters APIを利用できるようになる。
params.fetch(:blog, {}).permit(:title, :author)

例2: モデルクラスのaccepts_nested_attributes_forメソッドによって、関連付けられているレコードの更新や破棄を行えるようになります。この例は、idパラメータと_destroyパラメータに基づいています。

# `:id`と`:_destroy`が許可される
params.expect(author: [ :name, books_attributes: [[ :title, :id, :_destroy ]] ])

例3: integerキーを持つハッシュを異なる方法で処理し、属性を直接の子であるかのように宣言できます。accepts_nested_attributes_forhas_many関連付けを組み合わせると、以下のようなパラメータを得られます。

# 以下のようなデータが許可される:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}
params.expect(book: [ :title, chapters_attributes: [[ :title ]] ])

例4: 製品名を表すnameパラメータと、その製品に関連付けられた任意のdataハッシュがあり、製品名のname属性と、dataハッシュ全体(空ハッシュ{}で指定)を許可したい場合のシナリオを想定しています。

def product_params
  params.expect(product: [ :name, data: {} ])
end

cookieの概念は、Rails固有ではありません。cookie (HTTP cookieやWeb cookieとも呼ばれます)は、サーバーから送信されてユーザーのブラウザに保存される小さなデータです。

ブラウザ側では、cookieを保存することも、新しいcookieを作成したり、既存のcookieを変更することも、以後のリクエストでサーバーに送り返すことも可能です。Webリクエスト間のデータがcookieとして保存されることで、Webアプリケーションはユーザーの設定を記憶できるようになります。

Railsでは、cookiesメソッドを使うことで、ハッシュのようにcookieにアクセスできます。

class CommentsController < ApplicationController
  def new
    # cookieにコメント作者名が残っていたらフィールドに自動入力する
    @comment = Comment.new(author: cookies[:commenter_name])
  end

  def create
    @comment = Comment.new(comment_params)
    if @comment.save
      if params[:remember_name]
        # コメント作者名をcookieに保存する
        cookies[:commenter_name] = @comment.author
      else
        # コメント作者名がcookieに残っていたら削除する
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

cookieを削除するには、cookies.delete(:key)メソッドを使う必要があります。keynil値を設定してもcookieは削除されません。

cookieにスカラー値を渡した場合は、ユーザーがブラウザを閉じたときにそのcookieが削除されます。cookieを期限切れにする日時を指定したい場合は、cookieを設定するときに:expiresオプションを指定したハッシュを渡します。

たとえば、設定するCookieを1時間で失効させるには、次のようにします。

cookies[:login] = { value: "XJ-122", expires: 1.hour }

有効期限のないCookieを作成したい場合は、以下のようにcookieでpermanentメソッドを使います。これにより、割り当てられたcookieの有効期限が20年後に設定されます。

cookies.permanent[:locale] = "fr"

6.1 暗号化cookieと署名済みcookie

cookieはクライアントのブラウザに保存されるため、クライアントによって改ざんされる可能性があり、機密データを保存するうえで安全とは言えません。Railsでは、機密データの保存用に「署名付きcookie(signed cookie)」と「暗号化cookie(encrypted cookie)」を提供しています。

  • 署名付きcookieは、cookie値に暗号署名を追加して整合性を保護します。
  • 暗号化cookieは、署名に加えて値の暗号化も行うので、ユーザーは内容を読み取れません。

詳しくはCookies APIドキュメントを参照してください。

class CookiesController < ApplicationController
  def set_cookie
    cookies.signed[:user_id] = current_user.id
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2024
    redirect_to action: "read_cookie"
  end

  def read_cookie
    cookies.encrypted[:expiration_date] # => "2024-03-20"
  end
end

これらの特殊なcookieは、cookie値をシリアライザで文字列にシリアライズし、読み戻すときにRubyオブジェクトにデシリアライズします。利用するシリアライザはconfig.action_dispatch.cookies_serializerで指定できます。新規アプリケーションのデフォルトのシリアライザは:jsonです。

JSONでは、DateTimeSymbolなどのRubyオブジェクトのシリアライズのサポートに制約がある点にご注意ください。これらはすべてStringにシリアライズ/デシリアライズされます。

これらのオブジェクトや、さらに複雑なオブジェクトをcookieに保存する必要がある場合は、以後のリクエストでcookieを読み取るときに手動で値を変換する必要が生じる場合があります。

cookieセッションストアを使う場合、上記はsessionflashハッシュにも適用されます。

7 セッション

cookieはクライアント側(ブラウザ)に保存されますが、セッションデータはサーバー側(メモリ、データベース、またはキャッシュ)に保存されます。

セッションデータの有効期間は通常一時的であり(例: ブラウザを閉じるまで)、ユーザーのセッションに関連付けられます。セッションのユースケースの1つは、ユーザー認証などの機密データの保存です。

Railsアプリケーションでは、コントローラとビューでセッションを利用できます。

7.1 セッションにアクセスする

コントローラ内のセッションにアクセスするにはsessionインスタンスメソッドを利用できます。セッション値はハッシュに似たキーバリューペアとして保存されます。

class ApplicationController < ActionController::Base
  private
    # セッション内の`:current_user_id`キーを探索して現在の`User`を見つけるのに使う。
    # これはRailsアプリケーションでユーザーログインを扱う際の定番の方法
    # ログインするとセッション値が設定され、
    # ログアウトするとセッション値が削除される
    def current_user
      @current_user ||= User.find_by(id: session[:current_user_id]) if session[:current_user_id]
    end
end

セッションに何かを保存するには、ハッシュに値を追加するのと同じ要領でキーに代入できます。

以下の例では、ユーザーが認証されると、ユーザーのidがセッションに保存されて、以後のリクエストで使われるようになります。

class SessionsController < ApplicationController
  def create
    if user = User.authenticate_by(email: params[:email], password: params[:password])
      # セッションのuser idを保存し、
      # 以後のリクエストで使えるようにする
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end
end

セッションからデータの一部を削除するには、そのキーバリューペアを削除します。セッションからcurrent_user_idキーを削除する方法は、ユーザーをログアウトするときに一般に使われます。

class SessionsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    # 現在のユーザーもクリアする
    @current_user = nil
    redirect_to root_url, status: :see_other
  end
end

reset_sessionメソッドを使うと、セッション全体をリセットできます。セッション固定攻撃(session fixation attack)を回避するため、ログイン後にはreset_sessionを実行することが推奨されています。詳しくはセキュリティガイドを参照してください。

セッションは遅延読み込み(lazy load)されるため、アクションのコードでセッションにアクセスしない限り、セッションは読み込まれません。したがって、セッションを常に明示的に無効にする必要はなく、アクセスしないようにすれば十分です。

7.2 flash

Flashは、コントローラのアクション同士の間で一時的なデータを渡す方法を提供します。flashに配置したものはすべて、次のアクションで利用可能になり、その後クリアされます。

flashは、ユーザーにメッセージを表示するアクションにリダイレクトする前に、コントローラのアクションでメッセージ(通知やアラートなど)を設定するときによく使われます。

flashにアクセスするにはflashメソッドを使います。flashはセッションと同様にキーバリューペアとして保存されます。

たとえば、コントローラでユーザーをログアウトするアクションでは、次回のリクエストでコントローラがユーザーに表示できるflashメッセージを以下のように設定できます。

class SessionsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    flash[:notice] = "ログアウトしました"
    redirect_to root_url, status: :see_other
  end
end

ユーザーがアプリケーションで何らかの対話的操作を実行した後にメッセージで結果を表示することは、アクションの成功(もしくはエラーの発生)をユーザーにフィードバックする良い方法です。

flashでは、通常の:notice(通知)メッセージの他に、:alert(アラート)メッセージも表示できます。これらのflashメッセージには、意味を表す色をCSSで設定するのが普通です(例: 通知は緑、アラートはオレンジや赤)。

以下のようにredirect_toのパラメータにflashメッセージを追加することで、リダイレクト時にflashメッセージを表示することも可能です。

redirect_to root_url, notice: "ログアウトしました"
redirect_to root_url, alert: "問題が発生しました"

flashメッセージの種別は、noticealertだけではありません。:flash引数にキーを割り当てることで、flashに任意のキーを設定できます。

たとえば、:just_signed_upを割り当てるには以下のようにします。

redirect_to root_url, flash: { just_signed_up: true }

これでビューで以下の表示用のコードを書けるようになります。

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

上記のログアウトの例では、destroyアクションを実行するとアプリケーションのroot_urlにリダイレクトし、そこでflashメッセージを表示できます。ただし、このメッセージが自動的に表示されるわけではありません。直前のアクションでflashに設定した内容がどう扱われるかは、次に実行されるアクションで決定されます。

7.2.1 flashメッセージを表示する

直前のアクションでflashメッセージが設定された場合は、flashメッセージをユーザーに表示するのがよいでしょう。flashメッセージを表示する以下のようなHTMLをアプリケーションのデフォルトレイアウトに追加しておけば、flashメッセージが常に表示されるようになります。

以下はapp/views/layouts/application.html.erbにflashメッセージの表示コードを追加する例です。

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>
    <!-- (他にもコンテンツがあるとする) -->
    <%= yield %>
  </body>
</html>

上のnameは、noticealertなどのflashメッセージの種別を表します。この情報は通常、flashメッセージをどのようなスタイルでユーザーに表示するかを指定するのに使われます。

レイアウトファイルでnoticealertだけを表示するように制限したい場合は、nameでフィルタリングする方法が使えます。フィルタを行わない場合は、flashで設定されたすべてのキーが表示されます。

flashメッセージの読み取りと表示のコードをレイアウトファイルに含めておけば、flashを読み取るロジックを個別のビューに含めなくても、アプリケーション全体で自動的に表示されるようになります。

7.2.2 flash.keepflash.now

flash.keepは、flashの値を以後のリクエストにも引き継ぎたいときに使えます。このメソッドは、リダイレクトが複数回行われる場合に便利です。

たとえば、以下のコントローラのindexアクションがroot_urlに対応していて、ここでのリクエストをすべてUsersController#indexにリダイレクトするとします。 アクションがflashを設定してからMainController#indexにリダイレクトすると、flash.keepメソッドで別のリクエスト用の値を保持しておかない限り、別のリダイレクトが発生したときにflashの値は失われます。

class MainController < ApplicationController
  def index
    # すべてのflash値を保持する
    flash.keep
    # 以下のようにキーを指定すれば、特定の値だけを保持することも可能
    # flash.keep(:notice)
    redirect_to users_url
  end
end

flash.nowは、同じリクエストでflash値を利用可能にするときに使います。 flashに値を追加すると、デフォルトでは(現在のリクエストではなく)次回のリクエストでflashの値を利用可能になります。

たとえば、createアクションでリソースの保存に失敗し、newテンプレートを直接レンダリングした場合は、新しいリクエストが発生しないため、flashメッセージは表示されません。

しかし、そのような場合でもflashメッセージを表示したいことがあります。これを行うには、通常のflashと同じようにflash.nowを使うと、現在のリクエストでflashメッセージが表示されるようになります。

class ClientsController < ApplicationController
  def create
    @client = Client.new(client_params)
    if @client.save
      # ...
    else
      flash.now[:error] = "クライアントを保存できませんでした"
      render action: "new"
    end
  end
end

7.3 セッションストア

すべてのセッションには、セッションオブジェクトを表す一意のIDが存在し、これらのセッションIDはcookieに保存されます。実際のセッションオブジェクトは、次のいずれかの保存メカニズムを利用します。

ほとんどのセッションストアでは、サーバー上のセッションデータ(データベーステーブルなど)を検索するときに、cookie内にある一意のセッションidを使います。セッションIDをURLとして渡す方法は安全性が低いため、Railsでは利用できません。

7.3.1 CookieStore

CookieStoreはデフォルトで推奨されるセッションストアで、すべてのセッションデータをcookie自体に保存します(セッションIDも必要に応じて引き続き利用可能です)。CookieStoreは軽量で、新規Railsアプリケーションでは設定なしで利用できます。

CookieStoreには4KBのデータを保存できます。他のセッションストアに比べるとずっと小容量ですが、通常はこれで十分です。

利用するセッションストアの種類にかかわらず、セッションに大量のデータを保存することは推奨されていません。特に、モデルインスタンスのような複雑なオブジェクトをセッションに保存することは避けてください。

7.3.2 CacheStore

CacheStoreは、セッションに重要なデータを保存しない場合や、長期間の保存が不要な場合(例: メッセージングにflashだけを使う場合)に利用できます。これにより、アプリケーション用に構成したキャッシュ実装を利用してセッションが保存されるようになります。

CacheStoreのメリットは、追加のセットアップや管理を必要とせずに、既存のキャッシュインフラストラクチャを利用してセッションを保存できることです。 デメリットは、セッションの保存が一時的なものに限られ、いつでも消えてしまう可能性があることです。

セッションストレージについて詳しくはセキュリティガイドを参照してください。

7.4 セッションストレージの設定オプション

Railsでは、セッションストレージに関連するいくつかの設定オプションを利用できます。 利用するストレージの種類は、イニシャライザで以下のように設定できます。

Rails.application.config.session_store :cache_store

Railsは、セッションデータに署名するときにセッションキー(cookieの名前)を設定します。この動作もイニシャライザで変更できます。

Rails.application.config.session_store :cookie_store, key: "_your_app_session"

イニシャライザファイルの変更を反映するには、サーバーを再起動する必要があります。

以下のように:domainキーを渡して、cookieを使うドメイン名を指定することも可能です。

Rails.application.config.session_store :cookie_store, key: "_your_app_session", domain: ".example.com"

詳しくはRails設定ガイドのconfig.session_storeを参照してください。

Railsは、config/credentials.yml.encのセッションデータの署名に用いる秘密鍵をCookieStoreに設定します。この秘密鍵はbin/rails credentials:editコマンドで変更できます。

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...

CookieStoreを利用中にsecret_key_baseを変更すると、既存のセッションがすべて無効になります。既存のセッションをローテーションするには、Cookieローテーターの設定が必要です。

8 コントローラコールバック

訳注: Rails 7.2で従来のフィルタ(filter)という用語がアクションコールバック(action callback)に置き換えられ、その後さらにRails 8でコントローラコールバック(あるいは単にコールバック)に置き換えられました。

コントローラコールバック(controller callback)は、コントローラのアクションが実行される「直前(before)」、「直後(after)」、あるいは「直前と直後(around)」に実行されるメソッドです。

コントローラコールバックのメソッドは、特定のコントローラで定義することもApplicationControllerで定義することも可能です。すべてのコントローラはApplicationControllerを継承するので、ここで定義されたコールバックはアプリケーション内のすべてのコントローラで実行されます。

8.1 before_action

before_actionに登録したコールバックメソッドは、コントローラのアクションの直前に実行されます。リクエストサイクルを止めてしまう可能性があるのでご注意ください。before_actionの一般的なユースケースは、ユーザーがログイン済みであることを確認することです。

class ApplicationController < ActionController::Base
  before_action :require_login

  private
    def require_login
      unless logged_in?
        flash[:error] = "このセクションにアクセスするにはログインが必要です"
        redirect_to new_login_url # リクエストサイクルを停止する
      end
    end
end

このメソッドはエラーメッセージをflashに保存し、ユーザーがログインしていない場合にはログインフォームにリダイレクトします。

before_actionコールバックによってビューのレンダリングや前述の例のようなリダイレクトが行われると、コントローラのこのアクションは実行されなくなる点にご注意ください。コールバックの実行後に実行されるようスケジュールされた追加のコントローラコールバックが存在する場合は、これらもキャンセルされ、実行されなくなります。

上の例では、before_actionApplicationControllerで定義しているため、アプリケーション内のすべてのコントローラに継承されます。つまり、アプリケーション内のあらゆるリクエストでユーザーのログインが必須になります。

これは他の部分では問題ありませんが、「ログイン」ページだけは別です。「ログイン」操作はユーザーがログインしていない状態でも成功する必要があり、そうしておかないとユーザーがログインできなくなります。

skip_before_actionを使えば、特定のコントローラアクションでのみ指定のbefore_actionをスキップできます。

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

上のようにすることで、ユーザーがログインしていなくてもLoginsControllernewアクションとcreateアクションが成功するようになります。

特定のアクションでのみコールバックをスキップしたい場合には、:onlyオプションでアクションを指定します。逆に特定のアクションのみコールバックをスキップしたくない場合は、:exceptオプションでアクションを指定します。 これらのオプションはコールバックの追加時にも使えるので、最初の場所で選択したアクションに対してだけ実行されるコールバックを追加することも可能です。

同じコールバックを異なるオプションで複数回呼び出すと、最後に呼び出されたアクションコールバックの定義によって、それまでのコールバックの定義は上書きされます。

8.2 after_actionコールバックとaround_actionコールバック

コントローラアクションが実行される「直後」に実行するアクションコールバックは、after_actionで定義できます。 コントローラアクションが実行される「直前」と「直後」に実行するアクションコールバックは、around_actionで定義できます。

after_actionコールバックはbefore_actionコールバックに似ていますが、コントローラアクションがすでに実行済みのため、クライアントに送信されるレスポンスデータにアクセスできる点が異なります。

after_actionコールバックは、アクションが成功した場合にのみ実行されます。リクエストサイクルの途中で例外が発生した場合は実行されません。

around_actionコールバックは、コントローラアクションの直前と直後にコードを実行する場合に便利で、アクションの実行に影響する機能をカプセル化できます。これらは、関連するアクションをyieldで実行させる役割を担います。

たとえば特定のアクションのパフォーマンスを監視したいとします。以下のようにaround_actionを使うことで、各アクションが完了するまでにかかる時間を測定した情報をログに出力できます。

class ApplicationController < ActionController::Base
  around_action :measure_execution_time

  private
    def measure_execution_time
      start_time = Time.now
      yield  # ここでアクションが実行される
      end_time = Time.now

      duration = end_time - start_time
      Rails.logger.info "Action #{action_name} from controller #{controller_name} took #{duration.round(2)} seconds to execute."
    end
end

アクションコールバックには、上の例に示したようにcontroller_nameaction_nameが利用可能なパラメータとして渡されます。

around_actionコールバックはレンダリングもラップします。上の例では、ビューのレンダリングはdurationの値に含まれます。

around_actionyield以降のコードは、関連付けられたアクションで例外が発生すれば、コールバックにensureブロックが存在する場合でも実行されます(この振る舞いは、アクションで例外が発生するとafter_actionコードがキャンセルされるafter_actionコールバックとは異なります)。

8.3 コールバックのその他の利用法

before_actionafter_actionaround_actionの他にも、あまり一般的ではないコールバック登録方法が2つあります。

1つ目の方法は、*_actionメソッドに直接ブロックを渡すことです。このブロックはコントローラを引数として受け取ります。

たとえば、前述のrequire_loginアクションコールバックを書き換えてブロックを使うようにすると、以下のようになります。

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "このセクションにアクセスするにはログインが必要です"
      redirect_to new_login_url
    end
  end
end

このとき、コールバック内でsendメソッドを使っていることにご注意ください。 その理由は、logged_in?はprivateメソッドであり、そのままではコールバックがコントローラのスコープで実行されないためです(訳注: sendメソッドを使うとprivateメソッドを呼び出せます)。 この方法は、特定のコールバックを実装する方法としては推奨されませんが、もっとシンプルな場合には役に立つことがあるかもしれません。

特にaround_actionの場合、以下のコードのtime(&action)はコントローラアクションをブロックとしてtimeメソッドに渡します。

around_action { |_controller, action| time(&action) }

2つ目の方法は、コールバックアクションにクラス(または期待されるメソッドに応答する任意のオブジェクト)を指定することです。 これは、より複雑なコールバックコードをシンプルに書くときに便利です。

たとえば、around_actionコールバックを以下のように書き換えることで、渡したクラスを使って実行時間を測定できます。

class ApplicationController < ActionController::Base
  around_action ActionDurationCallback
end

class ActionDurationCallback
  def self.around(controller, action)
    start_time = Time.now
    yield  # ここでアクションが実行される
    end_time = Time.now

    duration = end_time - start_time
    Rails.logger.info "Action #{action} from controller #{controller} took #{duration.round(2)} seconds to execute."
  end
end

上の例では、ActionDurationCallbackクラスのメソッドはコントローラのスコープ内で実行されませんが、controlleractionを引数として受け取る点にご注目ください。

一般に、*_actionコールバックに渡すクラスは、そのコールバックと同じ名前のメソッドを実装する必要があります。つまり、たとえばbefore_actionコールバックに渡すクラスはbeforeメソッドを実装する必要があります。

また、aroundメソッドには、アクションを実行するためのyieldが必要です。

9 requestオブジェクトとresponseオブジェクト

すべてのコントローラにはrequestメソッドとresponseメソッドが必ず存在しているので、これらを使って現在のリクエストサイクルに関連付けられたリクエストオブジェクトとレスポンスオブジェクトにアクセスできます。

  • requestメソッドは、ActionDispatch::Request のインスタンスを返します。
  • responseメソッドは、ActionDispatch::Responseのインスタンスを返します。 これは、クライアント(ブラウザ)に返す内容を表すオブジェクトです(コントローラアクションのrenderredirectなど)。

9.1 requestオブジェクト

responseオブジェクトには、クライアントから受信したリクエストに関する有用な情報が多数含まれています。本セクションでは、requestオブジェクトの一部のプロパティの目的について説明します。

リクエストオブジェクトで利用可能なメソッドの完全なリストについては、Rails APIドキュメントのActionDispatch::RequestRack gemのドキュメントを参照してください。

requestのプロパティ 目的
host リクエストで使われるホスト名
domain(n=2) ホスト名の右(TLD:トップレベルドメイン)から数えてn番目のセグメント
format クライアントからリクエストされたContent-Typeヘッダー
method リクエストで使われるHTTPメソッド
get?post?patch?put?delete?head? HTTPメソッドがGET/POST/PATCH/PUT/DELETE/HEADのいずれかの場合にtrueを返す
headers リクエストに関連付けられたヘッダーを含むハッシュを返す
port リクエストで使われるポート番号(整数)
protocol プロトコル名に"://"を加えたものを返す("http://"など)
query_string URLの一部で使われるクエリ文字("?"より後の部分)
remote_ip クライアントのIPアドレス
url リクエストで使われるURL全体
9.1.1 query_parametersrequest_parameterspath_parameters

Railsのparamsには、クエリ文字列パラメータとしてURLに設定されたデータや、POSTリクエストのbodyとして送信されたデータなど、特定のリクエストに関するすべてのパラメータが集まっています。

requestオブジェクトでは、さまざまなパラメータにアクセスできる以下の3つのメソッドを利用できます。

  • query_parameters: クエリ文字列の一部として送信されたパラメータが含まれます。
  • request_parameters: POSTのbodyの一部として送信されたパラメータが含まれます。
  • path_parameters: ルーターによって特定のコントローラとアクションへのパスの一部であると解析されたパラメータが含まれます。

9.2 responseオブジェクト

responseオブジェクトは、アクションの実行中に、クライアント(ブラウザ)に送り返すデータをレンダリングすることでビルドされます。

通常はresponseオブジェクトを直接使うことはありませんが、after_actionコールバックなどでは、レスポンスに直接アクセスすると便利な場合があります。

responseオブジェクトのユースケースの1つは、Content-Typeヘッダーを設定することです。

response.content_type = "application/pdf"

responseオブジェクトの別のユースケースとして、カスタムヘッダーを設定するのにも使われます。

response.headers["X-Custom-Header"] = "some value"

headers属性は、ヘッダー名をヘッダー値に対応付けるハッシュです。Railsは、一部のヘッダーについては自動的に設定しますが、ヘッダーの更新やカスタムヘッダーの追加が必要な場合は、上の例のようにresponse.headersを利用できます。

headersメソッドには、コントローラから直接アクセスすることも可能です。

responseオブジェクトに含まれるプロパティの一部を以下に示します。

responseのプロパティ 目的
body クライアントに送り返されるデータの文字列(HTMLで最もよく使われる)
status レスポンスのステータスコード(200 OK、404 file not foundなど)
location リダイレクト先URL(存在する場合)
content_type レスポンスのContent-Typeヘッダー
charset レスポンスで使われる文字セット(デフォルトは"utf-8")
headers レスポンスで使われるヘッダー

リクエストオブジェクトで利用可能なメソッドの完全なリストについては、Rails APIドキュメントのActionDispatch::ResponseRack gemのドキュメントを参照してください。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加