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:
- Subclasse
ActiveRecord::ConnectionAdapters::ConnectionHandler
e sobrescrever os métodos responsáveis por recuperar pools de conexão - Crie uma classe completamente nova implementando a mesma API que
ConnectionHandler
- Acho que também é possível substituir apenas
retrieve_connection
método. Não me lembro onde está definido, mas acho que está emActiveRecord::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.