Este es un problema complicado, debido al estrecho acoplamiento dentro de ActiveRecord
, pero me las arreglé para crear una prueba de concepto que funciona. O al menos parece que funciona.
Algunos antecedentes
ActiveRecord
utiliza un ActiveRecord::ConnectionAdapters::ConnectionHandler
clase que es responsable de almacenar grupos de conexiones por modelo. De forma predeterminada, solo hay un grupo de conexiones para todos los modelos, porque la aplicación Rails habitual está conectada a una base de datos.
Después de ejecutar establish_connection
para una base de datos diferente en un modelo particular, se crea un nuevo grupo de conexiones para ese modelo. Y también para todos los modelos que puedan heredar de ella.
Antes de ejecutar cualquier consulta, ActiveRecord
primero recupera el grupo de conexiones para el modelo relevante y luego recupera la conexión del grupo.
Tenga en cuenta que la explicación anterior puede no ser 100% precisa, pero debería estar cerca.
Solución
Entonces, la idea es reemplazar el controlador de conexión predeterminado con uno personalizado que devolverá el grupo de conexiones según la descripción del fragmento proporcionado.
Esto se puede implementar de muchas maneras diferentes. Lo hice creando el objeto proxy que pasa nombres de fragmentos como ActiveRecord
disfrazado clases El controlador de conexión espera obtener el modelo AR y mira name
propiedad y también en superclass
recorrer la cadena jerárquica del modelo. He implementado DatabaseModel
clase que es básicamente el nombre del fragmento, pero se comporta como un modelo AR.
Implementación
Aquí hay un ejemplo de implementación. He usado la base de datos sqlite para simplificar, puede ejecutar este archivo sin ninguna configuración. También puede echar un vistazo a esta esencia
# 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
Creo que esto debería dar una idea de cómo implementar una solución lista para producción. Espero no haberme perdido nada obvio aquí. Puedo sugerir un par de enfoques diferentes:
- Subclase
ActiveRecord::ConnectionAdapters::ConnectionHandler
y sobrescribir los métodos responsables de recuperar grupos de conexiones - Cree una clase completamente nueva implementando la misma API que
ConnectionHandler
- Supongo que también es posible sobrescribir
retrieve_connection
método. No recuerdo dónde está definido, pero creo que está enActiveRecord::Core
.
Creo que los enfoques 1 y 2 son el camino a seguir y deberían cubrir todos los casos cuando se trabaja con bases de datos.