Detta är ett knepigt problem på grund av tät koppling inuti ActiveRecord
, men jag har lyckats skapa några proof of concept som fungerar. Eller åtminstone ser det ut som att det fungerar.
Lite bakgrund
ActiveRecord
använder en ActiveRecord::ConnectionAdapters::ConnectionHandler
klass som ansvarar för lagring av anslutningspooler per modell. Som standard finns det bara en anslutningspool för alla modeller, eftersom den vanliga Rails-appen är ansluten till en databas.
Efter att ha kört establish_connection
för olika databas i en viss modell skapas en ny anslutningspool för den modellen. Och även för alla modeller som kan ärva från det.
Innan du utför någon fråga, ActiveRecord
hämtar först anslutningspoolen för relevant modell och hämtar sedan anslutningen från poolen.
Observera att förklaringen ovan kanske inte är 100 % korrekt, men den bör vara nära.
Lösning
Så tanken är att ersätta standardanslutningshanteraren med en anpassad som kommer att returnera anslutningspoolen baserat på den angivna fragmentbeskrivningen.
Detta kan implementeras på många olika sätt. Jag gjorde det genom att skapa proxyobjektet som skickar shardnamn som förklädda ActiveRecord
klasser. Anslutningshanteraren förväntar sig att få AR-modell och tittar på name
egenskap och även i superclass
att gå modellhierarkikedjan. Jag har implementerat DatabaseModel
klass som i grunden är ett skärvnamn, men den beter sig som AR-modell.
Implementering
Här är exempel på implementering. Jag har använt sqlite databas för enkelhetens skull, du kan bara köra den här filen utan någon installation. Du kan också ta en titt på denna sammanfattning
# 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
Jag tror att detta borde ge en idé om hur man implementerar en produktionsklar lösning. Jag hoppas att jag inte har missat något uppenbart här. Jag kan föreslå ett par olika tillvägagångssätt:
- Underklass
ActiveRecord::ConnectionAdapters::ConnectionHandler
och skriv över de metoder som är ansvariga för att hämta anslutningspooler - Skapa en helt ny klass som implementerar samma API som
ConnectionHandler
- Jag antar att det också är möjligt att bara skriva över
retrieve_connection
metod. Jag kommer inte ihåg var den är definierad, men jag tror att den finns iActiveRecord::Core
.
Jag tror att tillvägagångssätt 1 och 2 är vägen att gå och bör täcka alla fall när man arbetar med databaser.