Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Alternando entre vários bancos de dados no Rails sem interromper transações


Este é um problema complicado, devido ao acoplamento apertado dentro do ActiveRecord , mas consegui criar uma prova de conceito que funciona. Ou pelo menos parece que funciona.

Alguns antecedentes

ActiveRecord usa um ActiveRecord::ConnectionAdapters::ConnectionHandler classe que é responsável por armazenar pools de conexão por modelo. Por padrão, há apenas um pool de conexão para todos os modelos, porque o aplicativo Rails usual está conectado a um banco de dados.

Após executar establish_connection para um banco de dados diferente em um modelo específico, um novo conjunto de conexões é criado para esse modelo. E também para todos os modelos que possam herdar dele.

Antes de executar qualquer consulta, ActiveRecord primeiro recupera o pool de conexões para o modelo relevante e, em seguida, recupera a conexão do pool.

Observe que a explicação acima pode não ser 100% precisa, mas deve ser próxima.

Solução

Portanto, a ideia é substituir o manipulador de conexão padrão por um personalizado que retornará o pool de conexões com base na descrição do estilhaço fornecida.

Isso pode ser implementado de muitas maneiras diferentes. Eu fiz isso criando o objeto proxy que está passando nomes de fragmentos como ActiveRecord disfarçados Aulas. O manipulador de conexão espera obter o modelo AR e analisa name propriedade e também em superclass para percorrer a cadeia hierárquica do modelo. Eu implementei DatabaseModel class que é basicamente o nome do shard, mas está se comportando como o modelo AR.

Implementação

Aqui está a implementação de exemplo. Eu usei o banco de dados sqlite para simplificar, você pode simplesmente executar este arquivo sem qualquer configuração. Você também pode dar uma olhada esta essência
# 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

Eu acho que isso deve dar uma idéia de como implementar a solução pronta para produção. Espero não ter perdido nada óbvio aqui. Posso sugerir algumas abordagens diferentes:
  1. Subclasse ActiveRecord::ConnectionAdapters::ConnectionHandler e sobrescrever os métodos responsáveis ​​por recuperar pools de conexão
  2. Crie uma classe completamente nova implementando a mesma API que ConnectionHandler
  3. Acho que também é possível substituir apenas retrieve_connection método. Não me lembro onde está definido, mas acho que está em ActiveRecord::Core .

Acho que as abordagens 1 e 2 são o caminho a seguir e devem abranger todos os casos ao trabalhar com bancos de dados.