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 :
- Sous-classe
ActiveRecord::ConnectionAdapters::ConnectionHandler
et écraser les méthodes responsables de la récupération des pools de connexion - Créer une toute nouvelle classe implémentant la même API que
ConnectionHandler
- 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 dansActiveRecord::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.