Mysql
 sql >> Base de données >  >> RDS >> Mysql

Basculer entre plusieurs bases de données dans Rails sans interrompre les transactions

C'est un problème délicat, en raison du couplage étroit à l'intérieur de ActiveRecord , mais j'ai réussi à créer une preuve de concept qui fonctionne. Ou du moins, il semble que cela fonctionne.

Quelques informations

ActiveRecord utilise un ActiveRecord::ConnectionAdapters::ConnectionHandler classe responsable du stockage des pools de connexions par modèle. Par défaut, il n'y a qu'un seul pool de connexion pour tous les modèles, car l'application Rails habituelle est connectée à une base de données.

Après avoir exécuté establish_connection pour différentes bases de données dans un modèle particulier, un nouveau pool de connexions est créé pour ce modèle. Et aussi pour tous les modèles qui pourraient en hériter.

Avant d'exécuter une requête, ActiveRecord récupère d'abord le pool de connexions pour le modèle concerné, puis récupère la connexion à partir du pool.

Notez que l'explication ci-dessus peut ne pas être précise à 100 %, mais elle devrait être proche.

Solution

L'idée est donc de remplacer le gestionnaire de connexion par défaut par un gestionnaire personnalisé qui renverra le pool de connexions en fonction de la description de la partition fournie.

Cela peut être mis en œuvre de différentes manières. Je l'ai fait en créant l'objet proxy qui transmet les noms de partition en tant que ActiveRecord déguisé Des classes. Le gestionnaire de connexion s'attend à obtenir le modèle AR et regarde name propriété et aussi à superclass pour parcourir la chaîne hiérarchique du modèle. J'ai implémenté DatabaseModel classe qui est essentiellement le nom du fragment, mais qui se comporte comme un modèle AR.

Mise en œuvre

Voici un exemple de mise en œuvre. J'ai utilisé la base de données sqlite pour plus de simplicité, vous pouvez simplement exécuter ce fichier sans aucune configuration. Vous pouvez également consulter cet essentiel

# 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

Je pense que cela devrait donner une idée de la façon de mettre en œuvre une solution prête pour la production. J'espère que je n'ai rien raté d'évident ici. Je peux suggérer deux approches différentes :

  1. Sous-classe ActiveRecord::ConnectionAdapters::ConnectionHandler et écraser les méthodes responsables de la récupération des pools de connexion
  2. Créer une toute nouvelle classe implémentant la même API que ConnectionHandler
  3. Je suppose qu'il est également possible d'écraser simplement retrieve_connection méthode. Je ne me souviens pas où il est défini, mais je pense que c'est dans ActiveRecord::Core .

Je pense que les approches 1 et 2 sont la voie à suivre et devraient couvrir tous les cas lorsque l'on travaille avec des bases de données.