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: "example@sqldat.com")
puts User.count
User.connection_handler.use_database("users_shard_2")
User.create(name: "Jane Doe", email: "example@sqldat.com")
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::ConnectionHandlere 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_connectionmetodo. 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.