[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 用のクラスを用意してそれを継承する形で使ったほうがよいです。
はぁ疲れた。
[rails] 任意の位置にエラーメッセージを表示するViewヘルパー
ActionView::Helpers::FormBuilder.module_eval do def error_message(attribute) @object.errors[attribute].map{|error| '<span class="error">' + error + '</span>'}.join("<br>").html_safe end end
これを使うと
<%= form_for(@user) do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.error_message :name %> <% end -%>
みたいに書ける
[rails] Rails3からSinatraを呼ぶ
Rails3からはSinatraとか他のRackベースアプリケーションが呼べるというのは様々な記事に載っているのですが、実例が見つからないので実際にやってみました。
1. railsプロジェクトを作成する
$ rails new rinatra
2. sinatraアプリケーションを作成する
$ mkdir sinatra $ emacs sinatra/app.rb
作成したsinatraアプリケーションは以下のとおり。っていうか単なるハローワールド
require 'rubygems' require 'sinatra' get '/hello' do "Hello Sinatra" end
$ cd rinatra $ vi Gemfile
source 'http://rubygems.org' gem 'rails', '3.0.0' gem 'sinatra' #<= これを書き足す # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3-ruby', :require => 'sqlite3' (以下略)
$ bundle install vendor/bundle
4. sinatraアプリケーションをrequireする
$ vi config/initializers/sinatra.rb
require "#{Rails.root}/../sinatra/app"
5. routesを設定してsinatraアプリケーションを呼べるようにする(ここ本命)
$ emacs config/routes.rb
Rinatra::Application.routes.draw do mount Sinatra::Application, :at => 'sinatra' #<= これを書き加える (以下略)
とここまでやったった上で rails server を起動して
http://localhost:3000/sinatra/helloにアクセスすると
sinatra のハローワールドが表示されました。
ちなみに rake routes はこんな感じ
$ rake routes (in /home/yalab/project/rinatra) sinatra /sinatra {:action=>"sinatra", :to=>Sinatra::Application}
ただ http://localhost:3000/sinatra にアクセスして sinatra の get '/' を呼ぶことはなぜかできませんでした。この辺り詳しい方いましたらコメントで教えていただけるとうれしいです。
ruby 1.9.2p0をインストールしたらgemが使えなくなった方へ
$ sudo rm -rf /usr/local/lib/ruby/site_ruby/1.9.1/*ubygem* $ cd PATH/TO/RUBY_SRC $ sudo make install
[rails] Rails3.0 beta3のmailでiso-2022-jp
世の中だいぶUTF-8が浸透して文字化けもあまり見なくなった昨今ですが、
未だUTF-8化してない悩ましいものの一つに日本語メールがあります。
rails3のActionMailer(というよりかはmail gem)はだいぶ良くなったのですが、
まだそれができなかったのでモンキーパッチを書いちゃいました。
# encoding: utf-8 require 'mail' Mail::UnstructuredField.module_eval do def encode_with_fix(value) encode_without_fix(value.encode(charset)) end alias_method_chain :encode, :fix end Mail::Message.module_eval do def charset=(value) @defaulted_charset = false @charset = value @header.charset = value @body.charset = value end end Mail::Body.module_eval do def encoded_with_fix(transfer_encoding = '8bit') dec = Mail::Encodings::get_encoding(encoding) if multipart? || transfer_encoding == encoding and dec.nil? encoded_without_fix(transfer_encoding) else enc = Mail::Encodings::get_encoding(get_best_encoding(transfer_encoding)) enc.encode(dec.decode(raw_source).encode(charset)) end end alias_method_chain :encoded, :fix end
このファイルをconfig/initializers以下に例えばmail_encoding.rbとでもして置いておいて
mailerで
class Notifier < ActionMailer::Base default :from => ADMIN_EMAIL, :charset => 'iso-2022-jp' def signup(account) @account = account mail :to => @account.email, :subject => t('mail_subject_notifier_signup') end end
のようにしてやるとiso-2022-jpでエンコードされたメールが送信できます。
っていうかMail::Bodyにcharsetを渡してない時点でそれなりに酷いバグじゃないかとか思ったりもするわけですが。
ちなみにmultipartなメールのテストはしてないのでアシカラズ。
[android] Softbank MMS On Froyo with ?.vodafone.ne.jp
モペログさんがせっかくMms.apkのUser-Agentを書き換えてSoftbankで使えるようにしてくれたのだけれど。未だに@k.vodafone.ne.jpのアドレスを使っている僕は残念ながら上手くMMSを使うことができなかった。
仕方がないので「ChimCity: Nexus Oneをアップデートしたら銀SIMでMMS送受信が出来たでござる」の記事を参考に自分でUserAgentを書き換えてやった。
書き換えたものはこちら
適用方法はモペログさんの記事を参考に
$ sudo adb remount $ sudo adb push Mms_V702NK.apk /system/app/Mms.apk
もちろんご使用は自己責任でお願いします。
[server] nginxのinitスクリプト
httpdのinitスクリプトを参考にnginxのinitスクリプトを書いた。
環境はCentOS 4.6なのでRedhat系なら使えます。
#!/bin/bash # Startup script for the Nginx Web Server # # chkconfig: - 85 15 # description: Nginx is a Light weight World Wide Web server. It is used to serve \ # HTML files and CGI. # processname: nginx # pidfile: /var/run/nginx.pid # config: /etc/nginx/nginx.conf . /etc/rc.d/init.d/functions nginx='/usr/local/nginx/sbin/nginx' prog=nginx RET=0 start () { echo -n $"Starting $prog: " daemon $nginx RET=$? echo return $RET } stop () { echo -n $"Stopping $prog: " killproc $nginx RET=$? echo return $RET } reload() { echo -n $"Reloading $prog: " killproc $nginx -HUP RET=$? echo return $RET } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; reload) reload ;; *) echo $"Usage: $prog {start|stop|restart|reload}" exit 1 ;; esac exit $RET
上のスクリプトを/etc/init.d/nginxとして保存して自動起動するようにする。
# chkconfig --add nginx # chkconfig nginx on # chkconfig httpd off //ついでにapacheが自動起動しないようにする