🐝

<Rails>sessionメソッドの実装について軽くコードリーディングしてみた

2022/06/29に公開

はじめに

session "メソッド" って言うけど、使われ方が全然メソッドっぽくない。

sessionメソッドは以下のようにして使われる。

session[:user_name] = user.name

まるでハッシュのようだ。

しかも

session.class
=> ActionDispatch::Request::Session

というふうにあたかもsessionというオブジェクトに対してclassメソッドが使えているように見える。

とはいえ、RailsチュートリアルやRailsガイドにもsession (インスタンス)メソッドと書いてあるので当然ながらメソッドである。

コントローラー内でbyebug↓

(byebug) defined? session
#=> "method"

続けてsessionとだけ打ってみる。

(byebug) session
#<ActionDispatch::Request::Session:0x00007ff84f3fc9c0 @by=#<ActionDispatch::Session::CookieStore:0x00007ff853201cc0 (長いので省略)>

sessionメソッドでActionDispatch::Request::Sessionのインスタンスが返る。

つまり、session[:user_name] というのはsessionメソッドの戻り値であるActionDispatch::Request::Sessionのインスタンスに対して[]= メソッドが呼び出せているということになる。

sessionにclassメソッドが使えているのは、sessionメソッドの戻り値であるActionDispatch::Request::Sessionのインスタンスに対してclassメソッドが呼び出せているという仕組み。

ということは、[]= メソッドがActionDispatch::Request::Session内に定義されているはずなのだが、ActionDispatch::Request::Sessionに行き着くまでにコードがどのような流れで処理されているのかをコードリーディングしたい。

コードリーディングして実装を調べてみる

trace_locationを使用してsession[:current_user_id] = 7というコードがどのような流れで処理されていくのかを調べてみる。

https://github.com/yhirano55/trace_location

TraceLocation.trace do
   session[:current_user_id] = 7
end

そうするとログが得られるので、内容を以下で要約。

処理の順番
1. ActionController::Metal#session2. Rack::Request::Helpers#session3. Rack::Request::Env#fetch_header4. ActionDispatch::Request::Session#[]=**

順番に見ていく。

ActionController::Metal#session

sessionメソッドが使われると、まずここに処理が飛ぶ。

delegate :session, to: "@_request"

https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal.rb#L157

delegateメソッド

delegateは以下のメソッドを自クラスに定義しているのと同じ。

def session
  @_request.session
end

つまりActionController::Metal内でsessionインスタンスメソッドが定義されてる。

ApplicationControllerがこのActionController::Metalを継承しているため、UsersController等の自作のコントローラー内でもsessionインスタンスメソッドが使用できるというわけだ。

@_requestは何者か?

@_requestはおそらく、ActionDispatch::Requestのインスタンスである。

なぜなら、sessionメソッドの実体は以下で見ていくRack::Request::Helpersに定義されているのだが、このモジュールをincludeしているのがActionDispatch::Requestしかないからだ。

https://github.com/rails/rails/blob/6291a9f6d36d77b32df7d07a8c7c56103a555d39/actionpack/lib/action_dispatch/http/request.rb#L19

Rack::Request::Helpers#session

def session
  fetch_header(RACK_SESSION) do |k|
    set_header RACK_SESSION, default_session
  end
end

@_request.sessionを実行するにあたり、次はsessionメソッドの実体を探しに行く。

(trace_locationを知るまでこのコードに辿り着けなくて苦労した、、)

fetch_headerってなんやんということで次。

Rack::Request::Env#fetch_header

def fetch_header(name, &block)
  @env.fetch(name, &block)
end

@envとは何かがわからなかったので以下を参考にする。

https://qiita.com/coe401_/items/ad7dc2f3e319c5beaf40#レシーバenvとは

@envはHTTPヘッダを表すハッシュ

nameRACK_SESSION

&blockset_header RACK_SESSION, default_session (厳密に言うとdo ~ endのかたまり)

fetchメソッド

Hash#[]と同じく、指定したkeyのvalueを取得する。

Hash#[]と違いkeyが無い場合nilではなく例外が発生する。

第二引数にデフォルト値を指定することが可能。key が存在しない場合はこのデフォルト値が返る。

book = { name: 'hoge', price: 1000 }
book.fetch(:name, "fooo")
=> 'hoge'

book.fetch(:author, "foobar")
=> 'foobar'

つまり@envというハッシュからname(つまりRACK_SESSION)というkeyを探し、存在しなければ&blockが返る。

trace_locationのログにset_headerに関するものがないため、(僕の環境で)sessionメソッドが走った時はRACK_SESSIONというkeyが存在していたということなのだろう。

ActionDispatch::Request::Session#[]=

def []=(key, value)
  load_for_write!
  @delegate[key.to_s] = value
end

ここで[]=メソッドの定義に処理がとぶ。

つまりはここまででsessionメソッドの処理が終わったということである。
sessionメソッドの戻り値はActionDispatch::Request::Sessionのインスタンスなので、RACK_SESSIONというkeyのvalueがActionDispatch::Request::Sessionのインスタンスであると推測できる。

@delegate

@delegateとはなんだろうか?処理を止めて調べてみる。

(byebug) @delegate
{"session_id"=>"f15808a61b80a6b1f8143482897524a2", "_csrf_token"=>"tqNAbzcy135NjTirnmZbtIjHcS9mAXQMWhku37T9iuk=", "current_user_id"=>7}

https://github.com/rails/rails/blob/7c1165c8c8752a84a9d02ef06fccbf113b8ff6b0/actionpack/lib/action_dispatch/request/session.rb#L69

@delegateにはsessionの中身が入っていた。

つまりは

session[:key] = value

でsessionにデータを追加したり更新できたりするということになる。

load_for_write!に関するログもあるが、お腹いっぱいのためここまで。

最後に

ずさんなところがあれば教えてください。全力で修正します。

Discussion