Jest to trudny problem z powodu ścisłego sprzężenia wewnątrz ActiveRecord
, ale udało mi się stworzyć jakiś dowód koncepcji, który działa. A przynajmniej wygląda na to, że działa.
Niektóre tło
ActiveRecord
używa ActiveRecord::ConnectionAdapters::ConnectionHandler
klasa, która jest odpowiedzialna za przechowywanie pul połączeń na model. Domyślnie jest tylko jedna pula połączeń dla wszystkich modeli, ponieważ zwykła aplikacja Railsowa jest połączona z jedną bazą danych.
Po wykonaniu establish_connection
dla innej bazy danych w danym modelu tworzona jest nowa pula połączeń dla tego modelu. A także dla wszystkich modeli, które mogą po nim odziedziczyć.
Przed wykonaniem zapytania ActiveRecord
najpierw pobiera pulę połączeń dla odpowiedniego modelu, a następnie pobiera połączenie z puli.
Pamiętaj, że powyższe wyjaśnienie może nie być w 100% dokładne, ale powinno być zbliżone.
Rozwiązanie
Pomysł polega więc na zastąpieniu domyślnego programu obsługi połączeń niestandardowym, który zwróci pulę połączeń na podstawie dostarczonego opisu fragmentu.
Można to zrealizować na wiele różnych sposobów. Zrobiłem to, tworząc obiekt proxy, który przekazuje nazwy fragmentów jako zamaskowane ActiveRecord
zajęcia. Program obsługi połączenia spodziewa się uzyskać model AR i patrzy na name
właściwość, a także w superclass
chodzić po hierarchicznym łańcuchu modelu. Wdrożyłem DatabaseModel
klasa, która jest zasadniczo nazwą fragmentu, ale zachowuje się jak model AR.
Wdrożenie
Oto przykładowa implementacja. Użyłem bazy danych sqlite dla uproszczenia, możesz po prostu uruchomić ten plik bez żadnej konfiguracji. Możesz również spojrzeć na ten opis
# 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
Myślę, że to powinno dać pomysł, jak wdrożyć gotowe rozwiązanie produkcyjne. Mam nadzieję, że nie przeoczyłam tu niczego oczywistego. Mogę zaproponować kilka różnych podejść:
- Podklasa
ActiveRecord::ConnectionAdapters::ConnectionHandler
i nadpisz metody odpowiedzialne za pobieranie pul połączeń - Stwórz zupełnie nową klasę implementującą to samo API co
ConnectionHandler
- Wydaje mi się, że można też po prostu nadpisać
retrieve_connection
metoda. Nie pamiętam, gdzie jest zdefiniowany, ale myślę, że jest wActiveRecord::Core
.
Myślę, że podejścia 1 i 2 są dobrym rozwiązaniem i powinny obejmować wszystkie przypadki podczas pracy z bazami danych.