Toto je ošemetný problém kvůli těsnému propojení uvnitř ActiveRecord
, ale podařilo se mi vytvořit nějaký důkaz konceptu, který funguje. Nebo to alespoň vypadá, že to funguje.
Nějaké pozadí
ActiveRecord
používá ActiveRecord::ConnectionAdapters::ConnectionHandler
třída, která je zodpovědná za ukládání fondů připojení na model. Ve výchozím nastavení je pro všechny modely pouze jeden fond připojení, protože obvyklá aplikace Rails je připojena k jedné databázi.
Po provedení establish_connection
pro jinou databázi v konkrétním modelu se pro tento model vytvoří nový fond připojení. A také pro všechny modely, které od něj mohou dědit.
Před provedením jakéhokoli dotazu ActiveRecord
nejprve načte fond připojení pro příslušný model a poté načte připojení z fondu.
Upozorňujeme, že výše uvedené vysvětlení nemusí být 100% přesné, ale mělo by být blízko.
Řešení
Cílem je tedy nahradit výchozí obslužnou rutinu připojení vlastním obslužným nástrojem, který bude vracet fond připojení na základě poskytnutého popisu fragmentu.
To lze implementovat mnoha různými způsoby. Udělal jsem to vytvořením objektu proxy, který předává názvy fragmentů jako maskované ActiveRecord
třídy. Obslužný program připojení očekává, že získá model AR a podívá se na name
vlastnost a také v superclass
procházet hierarchickým řetězcem modelu. Implementoval jsem DatabaseModel
třída, což je v podstatě název fragmentu, ale chová se jako model AR.
Implementace
Zde je příklad implementace. Pro jednoduchost jsem použil databázi sqlite, tento soubor můžete spustit bez jakéhokoli nastavení. Můžete se také podívat na tuto podstatu
# 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
Myslím, že by to mělo poskytnout představu, jak implementovat řešení připravené na výrobu. Doufám, že jsem zde nepřehlédl nic zjevného. Mohu navrhnout několik různých přístupů:
- Podtřída
ActiveRecord::ConnectionAdapters::ConnectionHandler
a přepište metody odpovědné za načítání fondů připojení - Vytvořte zcela novou třídu implementující stejné rozhraní API jako
ConnectionHandler
- Myslím, že je také možné jednoduše přepsat
retrieve_connection
metoda. Nepamatuji si, kde je definován, ale myslím, že je vActiveRecord::Core
.
Myslím, že přístupy 1 a 2 jsou správnou cestou a měly by pokrývat všechny případy při práci s databázemi.