ActiveRecord
内の緊密な結合のため、これはトリッキーな問題です。 、しかし、私はうまくいく概念実証を作成することができました。または、少なくとも機能しているように見えます。
いくつかの背景
ActiveRecord
ActiveRecord::ConnectionAdapters::ConnectionHandler
を使用します モデルごとの接続プールの保存を担当するクラス。通常のRailsアプリは1つのデータベースに接続されているため、デフォルトでは、すべてのモデルに対して1つの接続プールしかありません。
establish_connection
を実行した後 特定のモデルの異なるデータベースに対して、そのモデルに対して新しい接続プールが作成されます。また、それを継承する可能性のあるすべてのモデルについても。
クエリを実行する前に、ActiveRecord
最初に関連するモデルの接続プールを取得し、次にプールから接続を取得します。
上記の説明は100%正確ではないかもしれませんが、近いはずです。
解決策
したがって、デフォルトの接続ハンドラーを、提供されたシャードの説明に基づいて接続プールを返すカスタムハンドラーに置き換えるというアイデアがあります。
これは、さまざまな方法で実装できます。これは、シャード名を偽装したActiveRecord
として渡すプロキシオブジェクトを作成することで実現しました。 クラス。接続ハンドラーはARモデルを取得することを期待しており、name
を調べます プロパティおよびsuperclass
モデルの階層チェーンを歩きます。 DatabaseModel
を実装しました 基本的にシャード名ですが、ARモデルのように動作するクラスです。
実装
これが実装例です。簡単にするためにsqliteデータベースを使用しましたが、セットアップなしでこのファイルを実行できます。 この要点 もご覧ください。
# Define some required dependencies
require "bundler/inline"
gemfile(false) do
source "https://rubygems.org"
gem "activerecord", "~> 4.2.8"
gem "sqlite3"
end
require "active_record"
class User < ActiveRecord::Base
end
DatabaseModel = Struct.new(:name) do
def superclass
ActiveRecord::Base
end
end
# Setup database connections and create databases if not present
connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new({
"users_shard_1" => { adapter: "sqlite3", database: "users_shard_1.sqlite3" },
"users_shard_2" => { adapter: "sqlite3", database: "users_shard_2.sqlite3" }
})
databases = %w{users_shard_1 users_shard_2}
databases.each do |database|
filename = "#{database}.sqlite3"
ActiveRecord::Base.establish_connection({
adapter: "sqlite3",
database: filename
})
spec = resolver.spec(database.to_sym)
connection_handler.establish_connection(DatabaseModel.new(database), spec)
next if File.exists?(filename)
ActiveRecord::Schema.define(version: 1) do
create_table :users do |t|
t.string :name
t.string :email
end
end
end
# Create custom connection handler
class ShardHandler
def initialize(original_handler)
@original_handler = original_handler
end
def use_database(name)
@model= DatabaseModel.new(name)
end
def retrieve_connection_pool(klass)
@original_handler.retrieve_connection_pool(@model)
end
def retrieve_connection(klass)
pool = retrieve_connection_pool(klass)
raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
conn = pool.connection
raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
puts "Using database \"#{conn.instance_variable_get("@config")[:database]}\" (##{conn.object_id})"
conn
end
end
User.connection_handler = ShardHandler.new(connection_handler)
User.connection_handler.use_database("users_shard_1")
User.create(name: "John Doe", email: "[email protected]")
puts User.count
User.connection_handler.use_database("users_shard_2")
User.create(name: "Jane Doe", email: "[email protected]")
puts User.count
User.connection_handler.use_database("users_shard_1")
puts User.count
これにより、本番環境に対応したソリューションを実装する方法がわかるはずです。ここで明らかなことを見逃していないことを願っています。いくつかの異なるアプローチを提案できます:
- サブクラス
ActiveRecord::ConnectionAdapters::ConnectionHandler
接続プールの取得を担当するメソッドを上書きします -
ConnectionHandler
と同じAPIを実装する完全に新しいクラスを作成します -
retrieve_connection
を上書きすることも可能だと思います 方法。どこで定義されているかは覚えていませんが、ActiveRecord::Core
にあると思います 。
アプローチ1と2が進むべき道であり、データベースを操作する際のすべてのケースをカバーする必要があると思います。