在Rails中切换多个数据库而不破坏事务

11

我正在设置一个带有多个数据库的Rails应用程序。它使用ActiveRecord::Base.establish_connection db_config切换数据库(所有这些数据库都在database.yml中配置)。

establish_connection显然会在每次调用时破坏待处理的事务。其中一个负面影响是测试,必须禁用use_transactional_tests(导致测试变得缓慢且不理想)。

那么...如何使Rails应用程序同时在不同的数据库上维护多个事务? (澄清一下,我不是寻找高级的跨数据库事务。只是让数据库客户端,即Rails应用程序,能够同时维护多个事务,每个数据库一个。)

我看过唯一的解决方案是直接将establish_connection放在类定义中,但这假定你有一个专用于特定类的数据库。我正在应用基于用户的分片策略,其中单个记录类型分布在多个数据库中,因此数据库需要在代码中动态切换。


通常情况下,没有一种方法可以在多个数据库之间维护事务。这不是特定于任何语言的,而是大多数数据库引擎可以处理的限制。 - Nigel Ren
使用不同的用户? - inorganik
这些已经是不同的数据库。 - mahemoff
说实话,我觉得你的痛点表明你应该使用分布式数据库,比如Riak。我不是在钓鱼,只是一个观察者。 - engineerDave
@engineerdave 可能是真的,但我不喜欢将Rails应用程序迁移到非标准数据存储库以供AR使用的机会。 - mahemoff
显示剩余3条评论
1个回答

7
这是个棘手的问题,因为ActiveRecord内部紧密耦合,但我已经成功创建了一些概念验证的代码。至少看起来是这样的。
一些背景: ActiveRecord使用一个ActiveRecord::ConnectionAdapters::ConnectionHandler类来存储每个模型的连接池。默认情况下,所有模型只有一个连接池,因为通常Rails应用程序只连接一个数据库。
在特定模型中执行establish_connection后,将为该模型创建一个新的连接池。也会为可能从它继承的所有模型创建一个新的连接池。
在执行任何查询之前,ActiveRecord首先检索相关模型的连接池,然后从池中检索连接。
请注意,上述解释可能不完全准确,但它应该是接近的。
解决方案:
因此,想法是用自定义连接处理程序替换默认连接处理程序,该处理程序将根据提供的分片描述返回连接池。
可以以许多不同的方式实现此功能。我通过创建代理对象来实现,该对象将分片名称作为伪装的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: "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

我认为这应该可以让你了解如何实现生产就绪的解决方案。我希望我没有漏掉任何明显的东西。 我可以建议几种不同的方法:
  1. 子类ActiveRecord :: ConnectionAdapters :: ConnectionHandler 并覆盖负责检索连接池的方法
  2. 创建完全实现与 ConnectionHandler 相同api的新类
  3. 我想也可以仅覆盖 retrieve_connection 方法。 我记不清它在哪里定义,但我认为它在 ActiveRecord :: Core 中。
我认为1和2是最好的方法,并且应该涵盖所有使用数据库的情况。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接