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 が参考になりました。