Dit is een lastig probleem, vanwege de strakke koppeling binnen ActiveRecord
, maar ik ben erin geslaagd om een proof of concept te maken dat werkt. Of in ieder geval lijkt het alsof het werkt.
Wat achtergrond
ActiveRecord
gebruikt een ActiveRecord::ConnectionAdapters::ConnectionHandler
klasse die verantwoordelijk is voor het opslaan van verbindingspools per model. Standaard is er slechts één verbindingspool voor alle modellen, omdat de gebruikelijke Rails-app is verbonden met één database.
Na het uitvoeren van establish_connection
voor een andere database in een bepaald model wordt een nieuwe verbindingspool voor dat model gemaakt. En ook voor alle modellen die er van kunnen erven.
Voordat u een zoekopdracht uitvoert, ActiveRecord
haalt eerst de verbindingspool op voor het relevante model en haalt vervolgens de verbinding op uit de pool.
Merk op dat bovenstaande uitleg misschien niet 100% nauwkeurig is, maar het zou in de buurt moeten komen.
Oplossing
Het idee is dus om de standaardverbindingshandler te vervangen door een aangepaste die de verbindingspool retourneert op basis van de opgegeven shardbeschrijving.
Dit kan op veel verschillende manieren worden geïmplementeerd. Ik deed het door het proxy-object te maken dat shard-namen doorgeeft als vermomd ActiveRecord
klassen. Verbindingshandler verwacht AR-model te krijgen en kijkt naar name
eigendom en ook bij superclass
om de hiërarchieketen van het model te doorlopen. Ik heb DatabaseModel
geïmplementeerd klasse die in feite een Shard-naam is, maar het gedraagt zich als een AR-model.
Implementatie
Hier is een voorbeeldimplementatie. Ik heb de sqlite-database gebruikt voor de eenvoud, je kunt dit bestand gewoon uitvoeren zonder enige setup. Je kunt ook deze kern bekijken
# 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
Ik denk dat dit een idee zou moeten geven hoe een productieklare oplossing kan worden geïmplementeerd. Ik hoop dat ik hier niets voor de hand liggends heb gemist. Ik kan een aantal verschillende benaderingen voorstellen:
- Subklasse
ActiveRecord::ConnectionAdapters::ConnectionHandler
en overschrijf de methoden die verantwoordelijk zijn voor het ophalen van verbindingspools - Maak een geheel nieuwe klasse met dezelfde api als
ConnectionHandler
- Ik denk dat het ook mogelijk is om gewoon
retrieve_connection
te overschrijven methode. Ik weet niet meer waar het is gedefinieerd, maar ik denk dat het inActiveRecord::Core
staat .
Ik denk dat benaderingen 1 en 2 de beste keuze zijn en alle gevallen moeten dekken bij het werken met databases.