Ini adalah masalah yang rumit, karena kopling ketat di dalam ActiveRecord
, tetapi saya telah berhasil membuat beberapa bukti konsep yang berfungsi. Atau setidaknya sepertinya berhasil.
Beberapa latar belakang
ActiveRecord
menggunakan ActiveRecord::ConnectionAdapters::ConnectionHandler
kelas yang bertanggung jawab untuk menyimpan kumpulan koneksi per model. Secara default hanya ada satu kumpulan koneksi untuk semua model, karena aplikasi Rails biasa terhubung ke satu database.
Setelah menjalankan establish_connection
untuk database yang berbeda dalam model tertentu, kumpulan koneksi baru dibuat untuk model itu. Dan juga untuk semua model yang mungkin mewarisinya.
Sebelum menjalankan kueri apa pun, ActiveRecord
pertama-tama mengambil kumpulan koneksi untuk model yang relevan dan kemudian mengambil koneksi dari kumpulan.
Perhatikan bahwa penjelasan di atas mungkin tidak 100% akurat, tetapi harus mendekati.
Solusi
Jadi idenya adalah mengganti pengendali koneksi default dengan yang khusus yang akan mengembalikan kumpulan koneksi berdasarkan deskripsi pecahan yang disediakan.
Ini dapat diimplementasikan dengan berbagai cara. Saya melakukannya dengan membuat objek proxy yang meneruskan nama pecahan sebagai ActiveRecord
yang disamarkan kelas. Pengendali koneksi mengharapkan untuk mendapatkan model AR dan melihat name
properti dan juga di superclass
untuk berjalan dalam rantai hierarki model. Saya telah menerapkan DatabaseModel
class yang pada dasarnya adalah nama shard, tetapi berperilaku seperti model AR.
Implementasi
Berikut adalah contoh implementasi. Saya telah menggunakan database sqlite untuk kesederhanaan, Anda bisa menjalankan file ini tanpa pengaturan apa pun. Anda juga dapat melihat inti ini
# 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
Saya pikir ini harus memberikan ide bagaimana menerapkan solusi siap produksi. Saya harap saya tidak melewatkan sesuatu yang jelas di sini. Saya dapat menyarankan beberapa pendekatan berbeda:
- Subkelas
ActiveRecord::ConnectionAdapters::ConnectionHandler
dan timpa metode yang bertanggung jawab untuk mengambil kumpulan koneksi - Buat kelas yang benar-benar baru dengan mengimplementasikan api yang sama dengan
ConnectionHandler
- Saya rasa mungkin juga untuk menimpa
retrieve_connection
metode. Saya tidak ingat di mana itu didefinisikan, tapi saya pikir itu diActiveRecord::Core
.
Saya pikir pendekatan 1 dan 2 adalah cara yang harus dilakukan dan harus mencakup semua kasus saat bekerja dengan database.