session_store :active_record_store 時のデバッグTIPS
概要
rail3 で session_store :active_record_store とした場合、DBにsessionsテーブルが作成されるが、デバッグ目的にSELECTしてみても、BASE64エンコードされていて内容を簡単に確認できない。開発環境の場合、BASE64エンコードを止めて、デバッグしやすくするためのメモ。
結論としては、BASE64のencode/decodeしているコードを書き換えて、平文でDBに保存されるようにしました。以下そこに至るまでのメモ。
メモ
まず、MySQL5.6 には、TO_BASE64, FROM_BASE64があるので、SELECT FROM_BASE64(data) from sessions で行けそうだが、すべての環境がMySQL5.6では無いと思うし、環境(DB)に依存しない方法が無いか探ってみる。
理解のために、gems/activerecord-3.1.3/lib/active_record/session_store.rb を読むとBASE64でエンコードされていることがわかる。
rails3 の session に対する操作は、rack-1.3.5/lib/rack/session/abstract/id.rb で定義されていて、active_record_store の場合は activerecord-3.1.3/lib/active_record/session_store.rb で実装されている。
#/var/lib/gems/1.8/gems/rack-1.3.5/lib/rack/session/abstract/id.rb # Rack::Session::Abstract::ID 137 # ID sets up a basic framework for implementing an id based sessioning 138 # service. Cookies sent to the client for maintaining sessions will only 139 # contain an id reference. Only #get_session and #set_session are 140 # required to be overwritten. 141 # 142 # All parameters are optional. 143 # * :key determines the name of the cookie, by default it is 144 # 'rack.session' 145 # * :path, :domain, :expire_after, :secure, and :httponly set the related 146 # cookie options as by Rack::Response#add_cookie 147 # * :defer will not set a cookie in the response. 148 # * :renew (implementation dependent) will prompt the generation of a new 149 # session id, and migration of data to be referenced at the new id. If 150 # :defer is set, it will be overridden and the cookie will be set. 151 # * :sidbits sets the number of bits in length that a generated session 152 # id will be.
#/var/lib/gems/1.8/gems/activerecord-3.1.3/lib/active_record/session_store.rb 13 # The +session_id+ column should always be indexed for speedy lookups. 14 # Session data is marshaled to the +data+ column in Base64 format. 15 # If the data you write is larger than the column's size limit, 16 # ActionController::SessionOverflowError will be raised. <snip> 34 # You may provide your own session class implementation, whether a 35 # feature-packed Active Record or a bare-metal high-performance SQL 36 # store, by setting 37 # 38 # ActiveRecord::SessionStore.session_class = MySessionClass 39 40 # You must implement these methods: 41 # 42 # self.find_by_session_id(session_id) 43 # initialize(hash_of_session_id_and_data, options_hash = {}) 44 # attr_reader :session_id 45 # attr_accessor :data 46 # save 47 # destroy <snip> 51 class SessionStore < ActionDispatch::Session::AbstractStore 52 module ClassMethods # :nodoc: 53 def marshal(data) 54 ActiveSupport::Base64.encode64(Marshal.dump(data)) if data 55 end 56 57 def unmarshal(data) 58 Marshal.load(ActiveSupport::Base64.decode64(data)) if data 59 end <snip> #ActiveRecord::SessionStore::SqlBypass 260 def save 261 return false unless loaded? 262 marshaled_data = self.class.marshal(data) 263 connect = connection 264 265 if @new_record 266 @new_record = false 267 connect.update <<-end_sql, 'Create session' 268 INSERT INTO #{table_name} ( 269 #{connect.quote_column_name(session_id_column)}, 270 #{connect.quote_column_name(data_column)} ) 271 VALUES ( 272 #{connect.quote(session_id)}, 273 #{connect.quote(marshaled_data)} ) 274 end_sql 275 else 276 connect.update <<-end_sql, 'Update session' 277 UPDATE #{table_name} 278 SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} 279 WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} 280 end_sql 281 end 282 end
よって、任意のSession操作を行うためのクラスを書いて ActiveRecord::SessionStore.session_class で指定すれば良さそう。
ここで、負荷対策などのために凝ったことするのであれば自分で書いた方が良さそうだが、今回はデバッグできれば良いので、config/environments/development.rb の中で、marshal と unmarshal を書き換えてみる。
#config/environments/development.rb 1 <APPNAME>::Application.configure do 2 # Settings specified here will take precedence over those in config/application.rb 3 <snip> 31 config.after_initialize do 32 require 'active_record/session_store' 33 module ActiveRecord 34 class SessionStore 35 module ClassMethods 36 def marshal(data) 37 Marshal.dump(data) if data 38 end 39 def unmarshal(data) 40 Marshal.load(data) if data 41 end 42 end 43 end 44 end 45 end 46 end
これで試してみたら、無事読むことができました。
session[:test] = {:foo => :bar}
mysql> select * from sessions\G *************************** 1. row *************************** id: 1 session_id: 6c4dd632c07d36934013849810a95879 data:{:_csrf_token"1Gk+iPeVoIRWilIRulpoB3UKsh/QNdcZim91hd4SccAU=: test{foobar created_at: 2012-01-19 14:35:50 updated_at: 2012-01-19 14:35:50 1 row in set (0.00 sec)
データベースに格納できないデータを扱った場合の備えとして、BASE64をしていると思われるため、mershalを書き換えるのは、development のみにしています。 session テーブルを BINARY型にしておくと良いのかな?
rails3の実行環境ごとの設定方法については、 http://stackoverflow.com/questions/4800028/rails-3-setting-custom-environment-variables が参考になりました。