Questo è un problema complicato, a causa dell'accoppiamento stretto all'interno di ActiveRecord
, ma sono riuscito a creare un proof of concept che funziona. O almeno sembra che funzioni.
Un po' di background
ActiveRecord
utilizza un ActiveRecord::ConnectionAdapters::ConnectionHandler
classe responsabile della memorizzazione dei pool di connessione per modello. Per impostazione predefinita c'è un solo pool di connessioni per tutti i modelli, perché la solita app Rails è connessa a un database.
Dopo aver eseguito establish_connection
per database diversi in un particolare modello, viene creato un nuovo pool di connessioni per quel modello. E anche per tutti i modelli che ne possono ereditare.
Prima di eseguire qualsiasi query, ActiveRecord
prima recupera il pool di connessioni per il modello pertinente e quindi recupera la connessione dal pool.
Tieni presente che la spiegazione di cui sopra potrebbe non essere accurata al 100%, ma dovrebbe essere simile.
Soluzione
Quindi l'idea è quella di sostituire il gestore di connessione predefinito con uno personalizzato che restituirà il pool di connessioni in base alla descrizione dello shard fornita.
Questo può essere implementato in molti modi diversi. L'ho fatto creando l'oggetto proxy che sta passando i nomi di shard come ActiveRecord
mascherato classi. Il gestore della connessione si aspetta di ottenere il modello AR e guarda name
proprietà e anche in superclass
per percorrere la catena gerarchica del modello. Ho implementato DatabaseModel
classe che è fondamentalmente un nome shard, ma si comporta come un modello AR.
Attuazione
Ecco un esempio di implementazione. Ho usato il database sqlite per semplicità, puoi semplicemente eseguire questo file senza alcuna configurazione. Puoi anche dare un'occhiata a questo succo
# 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
Penso che questo dovrebbe dare un'idea di come implementare una soluzione pronta per la produzione. Spero di non aver perso nulla di ovvio qui. Posso suggerire un paio di approcci diversi:
- Sottoclasse
ActiveRecord::ConnectionAdapters::ConnectionHandler
e sovrascrivi quei metodi responsabili del recupero dei pool di connessione - Crea una classe completamente nuova implementando la stessa API di
ConnectionHandler
- Immagino che sia anche possibile sovrascrivere semplicemente
retrieve_connection
metodo. Non ricordo dove sia definito, ma penso che sia inActiveRecord::Core
.
Penso che gli approcci 1 e 2 siano la strada da percorrere e dovrebbero coprire tutti i casi quando si lavora con i database.