<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
というコードがどのような流れで処理されていくのかを調べてみる。
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"
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!に関するログもあるが、お腹いっぱいのためここまで。
最後に
ずさんなところがあれば教えてください。全力で修正します。
Discussion