<Rails>sessionメソッドの実装について軽くコードリーディングしてみた
はじめに
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#session ↓ 2. Rack::Request::Helpers#session ↓ 3. Rack::Request::Env#fetch_header ↓ 4. 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しかないからだ。
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
とは何かがわからなかったので以下を参考にする。
@env
はHTTPヘッダを表すハッシュ
name
はRACK_SESSION
&block
はset_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}
@delegate
にはsessionの中身が入っていた。
つまりは
session[:key] = value
でsessionにデータを追加したり更新できたりするということになる。
load_for_write!に関するログもあるが、お腹いっぱいのためここまで。