我正在设置一个包含多个数据库的 Rails 应用程序。它使用 ActiveRecord::Base.establish_connection db_config
在数据库之间切换(所有这些都在 database.yml 中配置)。
establish_connection
显然会中断每次调用的待处理事务。一个负面后果是测试,其中 use_transactional_tests
必须被禁用(导致不受欢迎的缓慢测试)。
那么……Rails 应用程序如何同时维护不同数据库上的多个事务? (澄清一下,我不是在寻找花哨的跨数据库事务。只是数据库客户端(即 Rails 应用程序)同时维护多个事务的一种方式,每个数据库一个。)
我见过的唯一解决方案是putting establish_connection
directly in the class definition ,但这假设您有一个专用于特定类的数据库。我正在应用基于用户的分片策略,其中单个记录类型分布在多个数据库中,因此需要在代码中动态切换数据库。
最佳答案
这是一个棘手的问题,因为 ActiveRecord
内部的紧密耦合,但我设法创建了一些可行的概念证明。或者至少看起来可行。
一些背景
ActiveRecord
使用 ActiveRecord::ConnectionAdapters::ConnectionHandler
类负责存储每个模型的连接池。默认情况下,所有模型只有一个连接池,因为通常的 Rails 应用程序连接到一个数据库。
在为特定模型中的不同数据库执行建立连接
后,会为该模型创建新的连接池。也适用于所有可能从它继承的模型。
在执行任何查询之前,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
实现相同 api 的全新类
- 我想也可以只覆盖
retrieve_connection
方法。我不记得它是在哪里定义的,但我认为它在ActiveRecord::Core
中。
我认为方法 1 和 2 是可行的方法,并且应该涵盖使用数据库时的所有情况。
关于mysql - 在 Rails 中的多个数据库之间切换而不中断事务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43691840/