[rails] ActiveRecord3の connection_pool の挙動を調べた
基本的にRailsでは単一のDBを使用するように設計されています。とはいえ負荷軽減のためだったり、あるいは様々なしがらみのために複数のDBに接続しなければいけない場合がたまにあったりすると思います。しかも残念な事に違うDBに同じ名前のテーブルがあったりしてどうすんだよコレとなることも無いとは言えないでしょう。
ActiveRecord が connection pool に対応したのは知識としては知っていましたが複数のDBに実際につなぎにいったばあい connection はどうなるの?と疑問に思ったので調べてみました。
# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 51行目 - 82行目 def self.establish_connection(spec = nil) case spec when nil raise AdapterNotSpecified unless defined?(Rails.env) establish_connection(Rails.env) when ConnectionSpecification self.connection_handler.establish_connection(name, spec) when Symbol, String if configuration = configurations[spec.to_s] establish_connection(configuration) else raise AdapterNotSpecified, "#{spec} database is not configured" end else spec = spec.symbolize_keys unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end begin require "active_record/connection_adapters/#{spec[:adapter]}_adapter" rescue LoadError => e raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" end adapter_method = "#{spec[:adapter]}_connection" if !respond_to?(adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end remove_connection establish_connection(ConnectionSpecification.new(spec, adapter_method)) end end
ActiveRecord::Base.establish_connection の実体は lib/active_record/connection_adapters/abstract/connection_specification.rb にありました。
establish_connection に文字列やシンボルを渡した場合それに相当する configuration から ConnectionSpecifiction のインスタンスを作成し、再び establish_connection を呼び出します。 そして establish_connection が ConnectionSpecification を受けとりますと今度は connection_handler の establish_connection を読んでいます。途中の過程はさておき connection_handler の establish_connection を呼びたいようです。
じゃあ connection_handler って何よ?と深追いしてみます。
# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 10行目 - 14行目 ## # :singleton-method: # The connection handler class_attribute :connection_handler, :instance_writer => false self.connection_handler = ConnectionAdapters::ConnectionHandler.new
同じファイルの14行めにそれはありました。ご丁寧にシングルトンですよとコメント付きです。そしてその実体は ConnectionAdapters::ConnectionHandler のインスタンスでした。
ということで舞台は ConnectionAdapters::ConnectionHandler クラスへ移ります。
# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_pool.rb 278行目 - 287行目 class ConnectionHandler attr_reader :connection_pools def initialize(pools = {}) @connection_pools = pools end def establish_connection(name, spec) @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) end
ConnectionAdapters::ConnectionHandler は new すると @connection_pools という変数をハッシュで初期化します。シングルトンインスタンスのインスタンス変数なのでこれもシングルトンです。そして establish_conection メソッドは ConnectionAdapters::ConnectionPool のインスタンスを作成し、name と紐付けて @connection_pools に格納しています。
はて? name って何だっけ? ということで ActiveRecord::Base.establish_connection に戻ってみますと name は仮引数や変数としては使われていないのでメソッド呼び出しであろうことがわかります。調べてみても name というメソッドは定義されていないので、 Module#name のことでしょう。リファレンスマニュアルによると「モジュールやクラスの名前を文字列で返します。」とありますので、establish_connectionをcallしたレシーバ名前の文字列です。
ということでシングルトンの @connection_pools に establish_connection のレシーバの数だけ connection_pool ができることになるということがわかりました。
自分で establish_connection をしない限りは rails の初期化時に ActiveRecord::Base.establish_connection が呼ばれるので通常はすべてのテーブルのアクセスが ActiveRecord::Base の connection_pool を使用することになります。
で、それが本当にそうなのか検証してみました。
require 'rubygems' require 'active_record' yml = <<EOS common: &common adapter: sqlite3 pool: 5 timeout: 5000 base: <<: *common database: base.sqlite3 one: <<: *common database: one.sqlite3 EOS ActiveRecord::Base.configurations = YAML.load(yml) class CreateTable < ActiveRecord::Migration def self.up unless connection.table_exists?(:users) create_table :users do |t| t.string :name t.integer :name t.timestamps end end end end [:base, :one].each{|env| ActiveRecord::Base.establish_connection(env) ActiveRecord::Migration.verbose = false CreateTable.up } class User < ActiveRecord::Base end class Ua < ActiveRecord::Base set_table_name 'users' establish_connection(:one) end class Ub < ActiveRecord::Base set_table_name 'users' establish_connection(:one) end class One < ActiveRecord::Base establish_connection(:one) end class U1 < One set_table_name 'users' end class U2 < One set_table_name 'users' end [User, Ua, Ub, U1, U2].each{|klass| p klass.name + ": " + klass.connection_pool.object_id.to_s } p ActiveRecord::Base.connection_handler.connection_pools.keys p Hash[ActiveRecord::Base.connection_handler.connection_pools.map{|k, v| [k, v.object_id]}]
実行結果
$ ruby connection_pool.rb "User: 75816690" "Ua: 75730560" "Ub: 75717530" "U1: 75799790" "U2: 75799790" ["ActiveRecord::Base", "One", "Ua", "Ub"] {"ActiveRecord::Base"=>75816690, "One"=>75799790, "Ua"=>75730560, "Ub"=>75717530}
establish_connection を行わない場合は ActiveRecord::Base の connection_pool を使用します。
クラスで直接 establish_connection を行った場合はそのクラスが独自に connection_pool を持つことになります。 establish_connection を行ったクラスを継承した場合は establish_connection を行ったスーパークラスの connection_pool を使うことになります。
ということで各クラスで establish_connection をしてしまうと connection_pool の最大値(デフォルトでは5) x クラスの数 だけconnectionが保持できてしまうので非常によろしくない状態になります。
何かしらのやんごとない理由で別DBに接続しなければいけない場合は connection_pool 用のクラスを用意してそれを継承する形で使ったほうがよいです。
はぁ疲れた。