我正在设置一个包含多个数据库的Rails应用程序。它使用ActiveRecord::Base.establish_connection db_config
在数据库之间切换(所有数据库都在database.yml中配置)。
establish_connection
显然会在每次通话时中断待处理的交易。一个负面结果是测试,其中必须禁用use_transactional_tests
(导致不合需要的慢速测试)。
那么...... Rails应用程序如何同时在不同的数据库上维护多个事务? (为了澄清,我并不是在寻找一个奇特的跨数据库事务。只是数据库客户端的一种方式,即Rails应用程序,可以同时维护多个事务,每个数据库一个。)
我见过的唯一解决方案是putting establish_connection
directly in the class definition,但假设您有一个专用于特定类的数据库。我正在应用基于用户的分片策略,其中单个记录类型分布在多个数据库中,因此需要在代码中动态切换数据库。
答案 0 :(得分:6)
这是一个棘手的问题,因为ActiveRecord
内部紧密耦合,但我设法创建了一些有效的概念证明。或者至少它看起来有效。
一些背景
ActiveRecord
使用ActiveRecord::ConnectionAdapters::ConnectionHandler
类,负责存储每个模型的连接池。默认情况下,所有模型只有一个连接池,因为通常的Rails应用程序连接到一个数据库。
对特定模型中的不同数据库执行establish_connection
后,将为该模型创建新的连接池。并且还适用于可能从中继承的所有模型。
在执行任何查询之前,ActiveRecord
首先检索相关模型的连接池,然后从池中检索连接。
请注意,上述说明可能不是100%准确,但应该接近。
<强>解决方案强>
因此,我们的想法是将默认连接处理程序替换为将根据提供的分片描述返回连接池的自定义连接处理程序。
这可以通过许多不同的方式实现。我是通过创建将分片名称作为伪装ActiveRecord
类传递的代理对象来完成的。期望连接处理程序获得AR模型并查看name
属性以及superclass
以查看模型的层次结构链。我已经实现了DatabaseModel
类,它基本上是分片名称,但它的行为类似于AR模型。
<强>实施强>
以下是示例实现。为简单起见,我使用了sqlite数据库,你可以在没有任何设置的情况下运行这个文件。您还可以查看this gist
# 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: "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
retrieve_connection
方法。我不记得它的定义,但我认为它在ActiveRecord::Core
。我认为方法1和方法2是可行的,应该涵盖使用数据库时的所有情况。