DbCharmer commit:b02576644d982f895365e796ba75b825f7104210 時点の README.rdoc のざっくり読み下し

"DbCharmer" は、シンプルでパワフルな ActiveRecordプラグインです。ActiveRecord が複数のデータベースや複数のデータベースサーバで動作できるよう拡張します。

このライブラリが ActiveRecord に追加する主な機能は以下のとおり:

  1. AR model のコネクションをシンプルに管理 (switch_connection_to メソッド)
  2. AR model をデータベースを分割したコネクションに
  3. クエリをどこにむかわせるか簡単に選べる機能 (Model.on_* メソッド群)
  4. 自動でマスタ・スレーブにクエリをむかわせる (参照はスレーブに、更新はマスタで)
  5. 複数のデータベースのマイグレーションをフレキシブルに
  6. 複数のシャーディング方法でシンプルなシャーディング機能を利用できる (value, range, mapping table)

詳細は、 http://dbcharmer.net を見てください。

導入

DbCharmer の導入には2つのアプローチがあります:

  • gem をつかう (オススメ。Rails 3.2以降ではこの方法でしか使えません)
  • Rails plugin として入れる (Rails 2.x でのみ使えます)

gem として導入するなら、Gemfile に以下を追加してください:

gem 'db-charmer', :require => 'db_charmer'

Railsプラグインとして導入するなら以下のコマンドで:

./script/plugin install git://github.com/kovyrin/db-charmer.git

注意: DbCharmer を Rails じゃないプロジェクトで使うなら、コネクションを管理するメソッドを使う前に、DbCharmer.env に適切な値を設定する必要があります。適切な値は、database.yml のトップレベルのセクション名です。

簡単な ActiveRecord のコネクションの管理

このプラグインで追加される機能として switch_connection_to というメソッドがありますが、これはいろんな種類の DB コネクションの指定を受け付け、モデルでその指定を使います。以下をサポートしています:

  1. 文字列とシンボル: database.yml にある接続設定のブロックを指定
  2. ActiveRecord のモデル (モデルに対してコネクションを設定する)
  3. デーテベースのコネクション (Model.connection)
  4. nil: デフォルトのコネクションにリセットする

サンプルコード:

class Foo < ActiveRecord::Model; end

Foo.switch_connection_to(:blah)
Foo.switch_connection_to('foo')
Foo.switch_connection_to(Bar)
Foo.switch_connection_to(Baz.connection)
Foo.switch_connection_to(nil)

サンプルの database.yml の設定:

production:
  blah:
    adapter: mysql
    username: blah
    host: blah.local
    database: blah

  foo:
    adapter: mysql
    username: foo
    host: foo.local
    database: foo

switch_connection_to メソッドは第二引数にオプション引数として should_exist = true な引数をとります。このオプションは文字列またはシンボルを引数にメソッドが呼ばれたとき、そのコネクションの設定が database.yml にあるかどうか見ます。この should_exist が true ならば、例外をあげるし、false ならばエラーは無視され、コネクションの変更は起きません。

これは、development モードやテストのときに、複数のデータベースをローカルに作りたくない、ひとつのデータベースに全部テーブルを作りたい、とかいうときにつかえます。

警告: 全てのコネクションのスイッチの呼び出しは、呼んだクラスだけでコネクションをスイッチします。switch_connection_to の呼び出しと、コネクションのスイッチを継承元のクラスでやってはいけません (たとえば ActiveRecord::Base クラスのコネクションをスイッチすると、全てのモデルが新しいコネクションになってしまいます。そういうことがしたいときには、普通に establish_connection を使いましょう)。

複数データベースのマイグレーション

複数のデータベースを使うアプリケーションでも、便利なスキーママイグレーションの仕組みが必要でしょう。
Rails ユーザのみなさんは既に Rails の migration という仕組みを持っています。DbCharmer でも、Rails の migration で可能なかぎりシームレスに複数データベースを扱えるようにしています。

複数のデータベースを操作するのに、以下の2つの方法が使えます:

  1. マイグレーションファイル全体でコネクションを変更する方法: マイグレーション全体を指定のデータベースにスイッチする
  2. ブロックでコネクションを変更する方法: マイグレーションの一部を指定のデータベースにスイッチする

マイグレーションファイルの例(全体でコネクションを変更する場合):

class MultiDbTest < ActiveRecord::Migration
  db_magic :connection => :second_db

  def self.up
    create_table :test_table, :force => true do |t|
      t.string :test_string
      t.timestamps
    end
  end

  def self.down
    drop_table :test_table
  end
end

マイグレーションファイルの例(ブロックでコネクションを変更する場合):

class MultiDbTest < ActiveRecord::Migration
  def self.up
    on_db :second_db do
      create_table :test_table, :force => true do |t|
        t.string :test_string
        t.timestamps
      end
    end
  end

  def self.down
    on_db :second_db { drop_table :test_table }
  end
end


マイグレーションファイルの例(全体でコネクションを変更し、同じテーブルの操作を複数コネクションに):
(注: :connection と :connections は、コネクションの配列を指定することができる)

class MultiDbTest < ActiveRecord::Migration
  db_magic :connections => [:second_db, :default]

  def self.up
    create_table :test_table, :force => true do |t|
      t.string :test_string
      t.timestamps
    end
  end

  def self.down
    drop_table :test_table
  end
end
デフォルトのマイグレーションのコネクションについて

DbCharmer 1.6.10 以降では、ActiveRecord::Migration.db_magic の呼び出しと、マイグレーションでのデフォルトのコネクションの設定(特定のコネクションの変更しない限りこれが使われる)ができるようになりました。マイグレーションでデフォルトの ActiveRecord のコネクションが使いたい場合は db_magic :connection => :default を使ってください。

不正なコネクション名の扱いについて

どの環境でもデフォルトでは、on_dbdb_magic の宣言は指定したコネクションが database.yml に存在しない場合失敗します。production 環境以外でひとつのデータベースを使うなどしているときに、こうなるのを無視させることができます(テストコード用のデータベースとかでとくに便利です)。

この振る舞いは、Rails の initializers で DbCharmer.connections_should_exist の設定をすることでで制御できます。

警告: もしテスト環境でデータベースのコネクションを分けてマスタ・スレーブのサポートをしたいなら、transactional fixtures のサポートをオフにする必要があります。そうしないと、データを使ったテストでいろいろ問題に遭遇することになるでしょう。

モデルをマスタ・スレーブ環境で使う

マスタ・スレーブレプリケーションは、今日の中規模から大規模なデータベースアプリケーションでの、もっともポピュラーなスケールアウト手法です。いくつかの Rails プラグインが、モデルでスレーブサーバを使えるようにしていますが、それらは大きなアプリケーションで使った感じではあまりフレキシブルではありません。

ActsAsReadonlyable プラグインをしばらく使ってきましたが、たくさんの変更を加えながら使っていました。しかし、このプラグインが作者に放置されたので、我々はマスタ・スレーブのためのコードをプラグインにまとめリリースすることに決めました(Rails 2.2以降向け)。DbCharmer は、以下の機能を Rails のモデルに追加します:

全ての参照をスレーブ(たち)に自動スイッチ

モデルを作ったら、db_magic :slave => :blahdb_magic :slaves => [ :foo, :bar ] のコマンドをモデルで使うことができます。そうすることで、find, count, exist などの参照系の操作をスレーブ(または複数のスレーブなら、それらをラウンドロビン)に切り替えるモードになります。例は以下のとおり:

class Foo < ActiveRecord::Base
  db_magic :slave => :slave01
end

class Bar < ActiveRecord::Base
  db_magic :slaves => [ :slave01, :slave02 ]
end
デフォルトコネクション切り替え

もし、一組のマスタ・スレーブ(あるいは単純にひとつより多いデータベース)がある環境ならば、いくつかのモデルでデフォルトのコネクションを変更したいかもしれません。db_magic :connection => :foo をモデルに書くことでそうすることができます。例:

class Foo < ActiveRecord::Base
  db_magic :connection => :foo
end

マスタ・スレーブ構成 (つまりメインのコネクション + スレーブのコネクション) で分割されているモデルの例:

class Bar < ActiveRecord::Base
  db_magic :connection => :bar, :slave => :bar_slave
end
クエリごとのコネクション管理

マスタで select のクエリを走らせたいというケースがあると思います。たとえば、ちょうどデータを追加して、それを読み出す必要があって、でもそれがスレーブに反映されたかどうかわからない、というようなときです。こういうようなときのために、設定する方法を ActiveRecord モデルに追加しています:

1) on_master - これは block スタイルと proxy スタイルの2種類の書き方があります。
block スタイルでは、コードブロックのコネクションを強制的にスイッチさせることができます:

User.on_master do
  user = User.find_by_login('foo')
  user.update_attributes!(:activated => true)
end

proxy スタイルは、あるクエリをマスタに向けることができます:

Comment.on_master.last(:limit => 5)
User.on_master.find_by_activation_code(code)
User.on_master.exists?(:login => login, :password => password)

2) on_slave - これはマスタを強制的に使うようにしたあとなどに、強制的にスレーブに使うようにさせるメソッドです。スレーブが複数あれば、ランダムでひとつが選ばれます。このメソッドも block と proxy のスタイルがあります。

3) on_db(connection) - これは前出の2つのメソッドを可能にするものです。これはモデルのコネクションを、コードのブロックや1つの文で、ある DB に切り替えるのに使います(2つの形式があります)。これは switch_connection_to と同じ類の値を受け付けます。例:

Comment.on_db(:olap).count
Post.on_db(:foo).find(:first)

development と test 環境のデフォルトでは、存在しないコネクションを on_db の呼び出しを使うと、クエリはひとつのデータベースに全てのクエリを送ります。production の on_db では、存在しない名前は受け付けません。

この振る舞いは、 DbCharmer.connections_should_existRails の初期化で設定することによって制御できます。

強制的なスレーブ参照

いくつかの場面で、デフォルトで、全ての参照がスレーブにむかうモードが使われるには重大すぎるモデルがありますが、それらをスレーブにむかわせるモードに切り替えたいと思うことがたまにあります。
たとえば、User モデルがあるとします。ユーザは古いアカウント情報を見たくないので、スレーブ参照での遅延は避けたいです。しかしあるケースでは(たとえば、ログアウト状態でのプロフィールページの表示、などなど)、全ての参照をスレーブに向けるよう切り替えるというのは合理的です。

このユースケースのために、DbCharmer の 1.7.0 から、強制的にスレーブを参照する機能が追加されました。これは、いくつかの小さな別々の機能から成りますが、組み合わせによりパワフルなものになっています:

1) ActiveRecorddb_magic メソッドの :force_slave_reads => false オプション。
このオプションはモデルで自動でスレーブ参照するのを無効にします。なので、on_slave あるいは、その他のスレーブ参照できるようにするメソッドを必要に応じて呼ぶ必要があります。例:

class User < ActiveRecord::Base
  db_magic :slave => slave01, :force_slave_reads => false
end

2) ActionControllerforce_slave_reads クラスメソッド。
このメソッドは、コントローラごと(引数なしで呼ばれたとき)、あるいは、アクションごと (:only:except パラメータを渡して呼んだとき) 、強制的にスレーブを参照します。
これは、スレーブ参照によるラグがいくらか許容できるアクションで、スレーブが設定されているモデルの参照先をスレーブに向わせる、というようなときに使えます。例:

class ProfilesController < Application
  force_slave_reads :except => [ :login, :logout ]
  ...
end

3) ActionControllerforce_slave_reads! というインスタンスメソッド、このメソッドで、アクション内、あるいは、コントローラのフィルタで、一時的に参照先を強制的にスレーブに向かわせられます。
このメソッドは同じアクションをログインしているユーザとログインしていないユーザに呼ばれるときに使えます。before_filter でユーザ認証して、認証していないユーザ向けには force_slave_reads! メソッドを呼びます。

class ProfilesController < Application
  before_filter do
    force_slave_reads! unless current_user
  end
  ...
end

注意: このメソッドを使う前に、DbCharmer の ActionController サポートを有効にする必要があります。
DbCharmer.enable_controller_magic! を、プロジェクトの初期化コードで呼ぶ必要があります。

4) DbCharmer.force_slave_reads メソッドは、ブロック内のコードで強制的にスレーブ参照をするのに使われます。これは、とても細かい粒度でのスレーブ参照を強制する機能です。例:

DbCharmer.force_slave_reads do
  ...
  total_users = User.count
  ...
end

注意: 今のところ、この機能はベータで、使う場合は注意が必要です。テストしているものの、現実世界のアプリケーションでは想定外の問題が起こる可能性があります。

関連のコネクションの管理

ActiveRecord モデルはそれぞれ互いに関連を持っていて、それぞれがデータベースのコネクションを持っているため、User.posts.count のようなメソッドチェーンなんかで、コネクションの管理をするのが少し難しいです。 posts の count を別のデータベースでやりたいならば、あるひとつのクラスだけコネクションを変更して、以下の様にメソッドを呼びます:

Post.on_db(:olap) { User.posts.count }

これはあまり良い書き方ではないようなので、関連をうまく扱えるように on_* メソッド群を準備しました。こんなかんじで使えます:

@user.posts.on_db(:olap).count
@user.posts.on_slave.find(:title => 'Hello, world!')

注意: ActiveRecord の関連は、結果のオブジェクトあるいはコレクションのプロキシとして実装されているため、チェインしたメソッド以外もコネクションの切り替えを使えます:

@post.user.on_slave   # post の author を返す
@photo.owner.on_slave # photo の owner を返す

DbCharmer の 1.4 以降では、has_many と HABTM 関連のコネクション切り替えで prefix 記法を使えます:

@user.on_db(:foo).posts
@user.on_slave.posts
named scope のサポート

DbCharmer ユーザの named scope でのコネクションの切り替えを簡単にするため、スコープでも on_* メソッドをサポートするようにしました。
以下のスコープチェーンは全部やってることは同じです(クエリは :foo データベースコネクションで実行されます):

Post.on_db(:foo).published.with_comments.spam_marked.count
Post.published.on_db(:foo).with_comments.spam_marked.count
Post.published.with_comments.on_db(:foo).spam_marked.count
Post.published.with_comments.spam_marked.on_db(:foo).count

関連のサポートもしているので、以下のようにも使えます:

@user.on_db(:archive).posts.published.all
@user.posts.on_db(:olap).published.count
@user.posts.published.on_db(:foo).first
一括したコネクションの管理

たくさんのテーブルを使ってコードを書きたい、そして、それらを別のデータベースを使うようにしたいというときがあります。
こんなふうにできます:

DbCharmer.with_remapped_databases(:logs => :big_logs_slave) { ... }

デフォルトが :logs のどのモデル (たとえば db_charmer :connection => :logs などと書いているモデル)も、このブロック内ではコネクションを :big_logs_slave に変えます。
これは、他のどの DbCharmer のメソッドより優先度が低いです。なので、Model.on_db(:foo).find(...) などは、指定されたデータベースを使いつづけ、そうでないものはリマップされます。

一度に、いくつでもリマップの指定をできます。また、DbCharmer でコネクションを指定していないどのモデルにマッチするデータベース名として :master を使えます。

ノート: DbCharmer は モデルで alias_method_chain を使って動いています。注意して、必要な箇所にだけパッチしています。
しかし、with_remapped_databases とデフォルトのデータベース(:master) のリマップを使いたいなら ActiveRecord::Base の全てのサブクラスにパッチするしかありません。
これはひどい問題も、大きなパフォーマンスの影響もおこさないはずです(よくできてる)。

シンプルなシャーディングのサポート

DbCharmer の 1.6.0 から、ActiveRecord の拡張としてシンプルなデータベースシャーディングをサポートしています。
この機能は本番環境でもテストされているとはいえ、この機能の基本をちゃんと理解せずにアプリケーションで使うのはオススメしません。

現時点では4つのシャーディング方法をサポートしています:

1) "range" - とてもシンプルなシャーディング方法で、事前に定義したプライマリキーの範囲でテーブル分割を行い、その小さく分割したテーブルを別のデータベースやデータベースサーバに配置することができます。
こんなとき便利: 非常に大きいテーブルがだんだん大きくなっていくようなとき。テーブルをいくつかのサーバにのせることで複雑なシャーディングの仕組みなしでシンプルさを保ちたいとき。

2) "hash_map" - けっこうシンプルなシャーディング方法で、事前に定義しておいたいくつかのキーでテーブル分割を行います(プライマリキーでなくてよい)。
たとえば、どの州をどのデータベース(あるいはデータベースサーバ)に保存するか定義しておいてアメリカの住所のリストを州ごとにシャーディングするとかできます。

3) "db_block_map" - これはとても複雑なシャーディング方法で、テーブルを小さなブロックのセットに分けて、さらに、シャーディングしたデータベースなりデータベースサーバなりにそれを割り当てます。
ブロックを追加したいときはいつでも、ブロックは自動かつシャーディング間でバランスをとってデータベースに割り当てられます。
この方法は、1000万から10億レコードの巨大なテーブルをスケールするのに使え、比較的簡単にシャーディングしなおせる一番よい方法です。

4) "db_block_group_map" - これは "db_block_map" とよく似た方法で、一点だけが異なります。それは、この方法は
データベースのセット(テーブルのグループ)を各サーバに持てて、各グループがシャーディングとして扱われるところです。
このアプローチはアプリケーションをスケールするまえに事前にシャーディングしておくのに使えます。
ひとつのサーバから簡単にはじめられます。10, 20, 50 に分割したデータベースを持っておき、ひとつのマシンで対応できなくなったら、それらのデータベースを別のサーバに移します。

どうやってシャーディングしたらよいか?

シャーディングの拡張を有効にするには、すこし準備が必要です:

1) シャーディングのコネクションを定義した Rails のイニシャライザを作る (スクリプトやアプリケーションの初期化のときのコード)。それぞれのコネクションに名前と、シャーディング方法と、初期化のためのそのオプションを指定します。

2) シャーディングを使いたいモデルにシャーディングのコネクションを明示する

3) シャーディングしたいモデルを操作する前にシャーディングを明示する

より詳細は、以下のドキュメントをご覧ください。

シャーディング(分割)コネクション

シャーディングコネクションはシンプルな抽象化で、クラスタのためのシャーディングパラメータを一箇所で指定し、このひとところで設定したものをモデルで使えるようにすることができます。
いくつかのシャーディングのコネクションの初期化の例は以下のとおりです:

1) range ベースのシャーディングコネクションの例:

TEXTS_SHARDING_RANGES = {
  0...100   => :shard1,
  100..200  => :shard2,
  :default  => :shard3
}

DbCharmer::Sharding.register_connection(
  :name => :texts,
  :method => :range,
  :ranges => TEXTS_SHARDING_RANGES
)

2) ハッシュマップのシャーディングコネクションの例:

SHARDING_MAP = {
  'US'  => :us_users,
  'CA'  => :ca_users,
  :default  => :other_users
}

DbCharmer::Sharding.register_connection(
  :name => :users,
  :method => :hash_map,
  :map => SHARDING_MAP
)

3) データベースブロックマップのシャーディングコネクションの例:

DbCharmer::Sharding.register_connection(
  :name => :social,
  :method => :db_block_map,
  :block_size => 10000,                   # ブロックごとのキーの数
  :map_table => :event_shards_map,        # シャーディングブロックをマップするテーブル
  :shards_table => :event_shards_info,    # シャーディングコネクション情報のテーブル
  :connection => :social_shard_info       # マップでどのコネクションを使うか
)

シャーディングコネクションの定義をしたら、モデルでそれを使えるようになります:

class Text < ActiveRecord::Base
  db_magic :sharded => {
    :key => :id,
    :sharded_connection => :texts
  }
end

class Event < ActiveRecord::Base
  set_table_name :timeline_events

  db_magic :sharded => {
    :key => :to_uid,
    :sharded_connection => :social
  }
end
シャーディングしたモデルでのコネクションの切り替えについて

シャーディングしたモデルを操作するとき、必要があればいつでも、どの分割先を使うか指定することができます。
シャーディングしていない環境での DbCharmer のつかいかたと似た方法で、クエリごとに接続を管理できるメソッドを用意しています:

Event.shard_for(10).find(:conditions => { :to_uid => 123 }, :limit => 5)
Text.shard_for(123).find_by_id(123)

その他に、range と hash_map のシャーディングの方法で、デフォルトの分割先に切り替えることができるメソッドがあります:

Text.on_default_shard.create(:body => 'hello', :user_id => 123)

そして最後に、システム内の各シャーディングで、コードを実行するメソッドがあります(現在のところ、db_block_map と db_block_group_map でのみサポートされています):

Event.on_each_shard { |event| event.delete_all }
自分のシャーディング方法の定義

DbCharmer は、ユーザ定義のシャーディング方法が可能です。自分のシャーディング案を実装するには、いくつかやることがあります:

1) DbCharmer::Sharding::Method::YourOwnName という名前をつけてクラスをつくる

2) 最低でも、コンストラクinitialize(config) と、判別するためのインスタンスメソッド shard_for_key(key)、これは database.yml ファイルか、rails connection アダプタのコネクションパラメータのハッシュからコネクション名を返すようなメソッドになるでしょう。

3) 以下のようにして、あなたのシャーディングコネクションを登録します:

DbCharmer::Sharding.register_connection(
  :name => :some_name,
  :method => :your_own_name,    # あなたの定義したシャーディング方法を小文字にしたシンボルで渡します
  ... ほかにパラメータが必要ならそれも ...
)

4) 他の標準のシャーディングコネクションと同様に、あなたの定義したシャーディングコネクションを使います

自分で定義したシャーディングで、デフォルトの分割先をサポートする方法

もし、on_default_shard メソッドを自分の定義したシャーディングモデルで使いたいなら、2つやることがあります:

1) support_default_shard? というインスタンスメソッドをシャーディングクラスに実装します。これは、デフォルト分割先の指定をサポートするなら true を、そうでないなら false を返します。

2) shard_for_key メソッドで、:default のシンボルをキーとしてサポートするよう実装します。

自分で定義したシャーディング方法で、シャーディング列挙をサポートするには

シャーディング列挙を自分で定義したシャーディングのモデルでサポートするには、shard_connections というインスタンスメソッドを追加する必要があります。このメソッドは、シャーディングコネクション名か、コネクションの接続で使われる設定の配列を返す必要があります。

ドキュメントと質問について

このライブラリについてのよりくわしい情報については、われわれのサイト http://dbcharmer.net を参照してください。DbCharmer の内部の詳細について知りたいときは、ソースコードをチェックアウトしてください。プラグインのコードは、だいたい100%のカバレッジがあります。test-project ディレクトリに全てのユニットテストと、実際に使うときのテストが入っています。

このプロジェクトについて何か質問があるときは、作者が使っている DbCharmer ユーザーズグループのメーリングリストにコンタクトすることができます:

動作する RubyRails について

この gem ライブラリは、いくつかの Ruby のバージョンと、Rails 2.3, 3.0, 3.1, 3.2 で CI しています。

CI は TravisCI.org (https://travis-ci.org/kovyrin/db-charmer) でまわしています。
ビルドステータスは https://travis-ci.org/kovyrin/db-charmer を見てください(原文では現在のステータスについてのバッジみたいな画像が表示されています)

今、以下の組み合わせでビルドしています:

  • Rails のバージョン:
    • 2.x
    • 3.0
    • 3.1
    • 3.2
  • Ruby のバージョン:
    • 1.8.7
    • Ruby Enterprise Edition (1.8.7)
    • 1.9.3 (Rails 3 のみ。それ以前はサポートしていないため)
  • データベース:

くわえて、この gem は Scribd.com の本番でも使われています (世界で最も大規模な Rails で作られたサイトのひとつです)。 REE + Rails 2.2, 2.3 あるいは Sinatra と Rack アプリケーションで使われています。

1.8.0 から、3.2.8 に対応しましたが、3.2.4 はオフィシャルにサポートしてないので注意してください。たぶん動くと思いますが、対応していないバージョンについてバグレポートはうけつけません。

作者について

このプラグインScribd.com 社内で使うために作られ、さらにそれ以外のみなさんに使ってもらうために公開されました。コードのほとんどは Oleksiy Kovyrin によって書かれ、MIT ライセンスで公開されています。詳細については LICENSE ファイルを参照してください。

その他のコントリビュータは以下のとおりです(アルファベット順):

  • Allen Madsen
  • Andrew Geweke
  • Ashley Martens
  • Cauê Guerra
  • David Dai
  • Dmytro Shteflyuk
  • Eric Lindvall
  • Eugene Pimenov
  • Jonathan Viney
  • Gregory Man
  • Michael Birk
  • Tyler McMullen




付記