Това е труден проблем, поради тясното свързване вътре в ActiveRecord
, но успях да създам някакво доказателство за концепция, която работи. Или поне изглежда, че работи.
Някои предистория
ActiveRecord
използва ActiveRecord::ConnectionAdapters::ConnectionHandler
клас, който е отговорен за съхраняване на пулове за връзки по модел. По подразбиране има само един пул за връзки за всички модели, тъй като обикновеното приложение Rails е свързано към една база данни.
След изпълнение на establish_connection
за различна база данни в конкретен модел се създава нов пул за връзки за този модел. А също и за всички модели, които могат да наследят от него.
Преди да изпълните каквато и да е заявка, ActiveRecord
първо извлича пул за връзки за съответния модел и след това извлича връзката от пула.
Обърнете внимание, че горното обяснение може да не е 100% точно, но трябва да е близко.
Решение
Така че идеята е да се замени манипулатора на връзка по подразбиране с персонализиран такъв, който ще връща пул за връзки въз основа на предоставеното описание на фрагмента.
Това може да се приложи по много различни начини. Направих го, като създадох прокси обекта, който предава имена на фрагменти като прикрит ActiveRecord
класове. Манипулаторът на връзката очаква да получи AR модел и разглежда name
свойство, а също и в superclass
да вървят по йерархичната верига на модела. Внедрих DatabaseModel
клас, който по същество е име на фрагмент, но се държи като AR модел.
Внедряване
Ето примерна реализация. Използвах sqlite база данни за простота, можете просто да стартирате този файл без никаква настройка. Можете също да разгледате този същност
# 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
Мисля, че това трябва да даде идея как да се приложи готово за производство решение. Надявам се, че не съм пропуснал нищо очевидно тук. Мога да предложа няколко различни подхода:
- Подклас
ActiveRecord::ConnectionAdapters::ConnectionHandler
и презапишете тези методи, отговорни за извличането на пулове за връзки - Създайте напълно нов клас, внедряващ същия API като
ConnectionHandler
- Предполагам също така е възможно просто да се презапише
retrieve_connection
метод. Не помня къде е дефиниран, но мисля, че е вActiveRecord::Core
.
Мисля, че подходи 1 и 2 са правилният начин и трябва да обхващат всички случаи при работа с бази данни.