Dette er et vanskeligt problem på grund af tæt kobling inde i ActiveRecord
, men jeg har formået at skabe noget proof of concept, der virker. Eller i det mindste ser det ud til, at det virker.
Noget baggrund
ActiveRecord
bruger en ActiveRecord::ConnectionAdapters::ConnectionHandler
klasse, der er ansvarlig for at opbevare forbindelsespuljer pr. model. Som standard er der kun én forbindelsespulje for alle modeller, fordi den sædvanlige Rails-app er forbundet til én database.
Efter at have udført establish_connection
for forskellige databaser i en bestemt model, oprettes en ny forbindelsespulje for den model. Og også for alle modeller, der måtte arve fra det.
Før du udfører en forespørgsel, ActiveRecord
henter først forbindelsespuljen til relevant model og henter derefter forbindelsen fra poolen.
Bemærk, at ovenstående forklaring måske ikke er 100 % nøjagtig, men den bør være tæt på.
Løsning
Så ideen er at erstatte standardforbindelseshandleren med en brugerdefineret, der returnerer forbindelsespuljen baseret på den angivne shard-beskrivelse.
Dette kan implementeres på mange forskellige måder. Jeg gjorde det ved at oprette proxy-objektet, der sender shard-navne som forklædte ActiveRecord
klasser. Forbindelseshandler forventer at få AR-model og ser på name
ejendom og også på superclass
at gå i modelhierarkikæden. Jeg har implementeret DatabaseModel
klasse, der grundlæggende er et shard navn, men den opfører sig som AR-model.
Implementering
Her er et eksempel på implementering. Jeg har brugt sqlite database for enkelhedens skyld, du kan bare køre denne fil uden nogen opsætning. Du kan også tage et kig på denne oversigt
# 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
Jeg tror, at dette burde give en idé om, hvordan man implementerer en produktionsklar løsning. Jeg håber ikke, jeg gik glip af noget åbenlyst her. Jeg kan foreslå et par forskellige tilgange:
- Underklasse
ActiveRecord::ConnectionAdapters::ConnectionHandler
og overskriv de metoder, der er ansvarlige for at hente forbindelsespuljer - Opret en helt ny klasse, der implementerer det samme API som
ConnectionHandler
- Det er vel også muligt bare at overskrive
retrieve_connection
metode. Jeg kan ikke huske, hvor det er defineret, men jeg tror, det er iActiveRecord::Core
.
Jeg tror, tilgang 1 og 2 er vejen at gå og bør dække alle tilfælde, når man arbejder med databaser.