Post

Railsのリクエストライフサイクルのについて

背景: Rails API での current_user について

この間、Rails API を勉強した時、下記のコードに引っかかった。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Api::V1::BaseController < ApplicationController
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate

  def authenticate
    authenticate_or_request_with_http_token do |token, _options|
      @_current_user ||= ApiKey.active.find_by(access_token: token)&.user
    end
  end

  def current_user
    @_current_user
  end
end

このコードの意図は、API 側で current_user を使えるようにすること。

気になるのは、ここの||=演算子。

a ||= xxx > 「||」演算子の自己代入演算子。a が 偽 か 未定義 なら a に xxx を代入する、という意味になります。

1
@_current_user ||= ApiKey.active.find_by(access_token: token)&.user

直感で考えると、ユーザーがログインしている状態だったら、
同じユーザーからリクエストが来る時、毎回 token を検証し、 DB にクエリしてユーザーを特定する必要がなくなるではないか。

だったら、疑問がある。

確か、HTTP リクエストはステートレスで、session や token などを使って、ユーザー状態を保持する仕組みになっていると思うけど、

もし、ここでaccess_tokenを都度検証しない場合、どうやって同じユーザーだとわかるのか?

他のユーザーからリクエストがきたら、同じcurrent_userと判断されないのか?

もしかしたら、Rails controller は他の仕組みがあって、同じユーザーからのリクエストだとわかっているのか?

通常のcurrent_userの使い方について

似たような書き方はは Rails Tutorial の時に見たことがある。

1
2
3
4
5
6
7
module SessionsHelper
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    end
  end
end

ここは、View 側でcurrent_userを使うため、helper method として作ったもの。

current_user メソッドが1リクエスト内の処理で何度も呼び出されてしまうと、呼び出された回数と同じだけデータベースへの問い合わせが発生してしまい、結果として処理が完了するまでに時間がかかってしまうからです。

User.find_by の実行結果をインスタンス変数に保存することで、1リクエスト内におけるデータベースへの問い合わせは最初の1回だけになり、以後の呼び出しではインスタンス変数の結果を再利用するようになります

Rails Tutorial の説明によると、ここのcurrent_userは同じリクエスト内に使うもの。

つまり、同じリクエスト内で、ユーザーが特定できたら、View 側で何度もcurrent_userを呼び出しても、都度の DB クエリが必要なくなる。

だとしたら、やはり異なるリクエストなら、current_user が同じユーザーかどうかという判断はできない?

リクエストが来るとき、Rails はどう動いているのか

それなら、ユーザーからリクエストが来るとき、Rails は実際にどう対応しているのか。

調べたら、下記のリソースが見つけた。

The Lifecycle of a Request

Storing state across requests from the same user

仮に、get '/posts' => 'posts#index'という route がある。

そして、get '/posts'というリクエストが来るとき、Rails の対応は簡単にまとめると、こんな感じになる

  1. controller = PostsController.newで PostsController のインスタンスが作成される。 この controller には、三つのインスタンス変数が持つ - @request@response@params

  2. controller はindexアクションを実行する。

  3. もしindexアクションで view テンプレート を render するなら、controller が持つインスタンス変数が view のインスタンスにコピーして渡される。

  4. view インスタンスというのは、そのテンプレートのself。view ファイル内で使う変数は view インスタンスが controller または他の view インスタンスらコピした変数。

  5. indexアクションの内容を実行完了後、controller インスタンスの任務が完了したので、そのまま廃棄される

  6. API の場合だと、もし最後の実行内容は JSON 形式のレスポンスを返すことなら、それを返した時点で、このリクエストの対応が完了になったので、controller インスタンスが捨てられる。

  7. 次のリクエストが来る時、また新しい controller インスタンスが作成られ、そのリクエストを対応する。

Rails controller のインスタンス変数のスコープとライフサイクル

そうしたら、ここでの実行流れはこんな感じになると思う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Api::V1::BaseController < ApplicationController
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate

  def authenticate
    authenticate_or_request_with_http_token do |token, _options|
      @_current_user ||= ApiKey.active.find_by(access_token: token)&.user
    end
  end

  def current_user
    @_current_user
  end
end

もしget '/posts'というリクエストが来たら、

  1. PostController のインスタンスが生成される。
  2. indexアクションを実行する前に、親クラスから継承したbefore_actionを実行する。
  3. そして、@_current_userが初期化され、初めて値が代入される。
  4. このリクエストが処理終了するまで、PostController のindexアクション内及びindexアクションによる呼び出される関連のメソッド内で、current_userを使って、ユーザーを取得できる
  5. indexアクションが実行完了されたら、PostController のインスタンスが廃棄される。その同時に、@_current_userインスタンス変数も当然捨てられる。

controller が持つインスタンス変数は、同じリクエスト内でも、関連する view にコピして渡すことができる以外

他の controller や他の controller の view、またはモデルでも利用することができない。

ここの||=演算子について

最初の疑問に戻ると、

  • Rails controller は異なるリクエストが同じユーザーから来たかどうか、token を検証する前に判断できない
  • つまり、リクエストが来る度に、token の都度検証が必要。
  • そのため、新しいリクエストが来たら、 @_current_userが nil になっているので、ApiKey.active.find_by(access_token: token)&.userが必ず実行される

こう見ると、ここでの||=演算子は普通の=と同じ結果..

参照

why cannot reassign class variables in controller of rails

Is Rails shared-nothing or can separate requests access the same runtime variables?

What is the scope of an instance variable in a Rails controller?

Lifecycle of Instance variables in Ruby on Rails

Ruby on Rails Controller Instance Variable not shared

Sharing data between threads

This post is licensed under CC BY 4.0 by the author.