sql >> Base de Datos >  >> RDS >> Mysql

Cambiar entre múltiples bases de datos en Rails sin romper transacciones

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:

  1. Subclase ActiveRecord::ConnectionAdapters::ConnectionHandler y sobrescribir los métodos responsables de recuperar grupos de conexiones
  2. Cree una clase completamente nueva implementando la misma API que ConnectionHandler
  3. Supongo que también es posible sobrescribir retrieve_connection método. No recuerdo dónde está definido, pero creo que está en ActiveRecord::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.