这是个棘手的问题,因为
ActiveRecord
内部紧密耦合,但我已经成功创建了一些概念验证的代码。至少看起来是这样的。
一些背景:
ActiveRecord
使用一个
ActiveRecord::ConnectionAdapters::ConnectionHandler
类来存储每个模型的连接池。默认情况下,所有模型只有一个连接池,因为通常Rails应用程序只连接一个数据库。
在特定模型中执行
establish_connection
后,将为该模型创建一个新的连接池。也会为可能从它继承的所有模型创建一个新的连接池。
在执行任何查询之前,
ActiveRecord
首先检索相关模型的连接池,然后从池中检索连接。
请注意,上述解释可能不完全准确,但它应该是接近的。
解决方案:
因此,想法是用自定义连接处理程序替换默认连接处理程序,该处理程序将根据提供的分片描述返回连接池。
可以以许多不同的方式实现此功能。我通过创建代理对象来实现,该对象将分片名称作为伪装的
ActiveRecord
类传递。连接处理程序希望获取AR模型并查看
name
属性以及
superclass
来遍历模型的层次链。我实现了
DatabaseModel
类,它基本上是分片名称,但它的行为类似于AR模型。
实现:
这里是一个示例实现。我使用sqlite数据库来简化,您可以直接运行此文件而无需进行任何设置。您还可以查看
此代码片段。
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
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
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: "john.doe@example.org")
puts User.count
User.connection_handler.use_database("users_shard_2")
User.create(name: "Jane Doe", email: "jane.doe@example.org")
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是最好的方法,并且应该涵盖所有使用数据库的情况。