Đây là một vấn đề phức tạp, do khớp nối chặt chẽ bên trong ActiveRecord
, nhưng tôi đã cố gắng tạo ra một số bằng chứng về khái niệm hoạt động. Hoặc ít nhất có vẻ như nó hoạt động.
Một số thông tin cơ bản
ActiveRecord
sử dụng ActiveRecord::ConnectionAdapters::ConnectionHandler
lớp chịu trách nhiệm lưu trữ các nhóm kết nối cho mỗi mô hình. Theo mặc định, chỉ có một nhóm kết nối cho tất cả các mô hình, vì ứng dụng Rails thông thường được kết nối với một cơ sở dữ liệu.
Sau khi thực hiện establish_connection
cho các cơ sở dữ liệu khác nhau trong mô hình cụ thể, nhóm kết nối mới được tạo cho mô hình đó. Và cũng cho tất cả các mô hình có thể kế thừa từ nó.
Trước khi thực hiện bất kỳ truy vấn nào, hãy ActiveRecord
trước tiên truy xuất nhóm kết nối cho mô hình có liên quan và sau đó truy xuất kết nối từ nhóm.
Lưu ý rằng giải thích trên có thể không chính xác 100%, nhưng nó phải gần gũi.
Giải pháp
Vì vậy, ý tưởng là thay thế trình xử lý kết nối mặc định bằng trình xử lý tùy chỉnh sẽ trả về nhóm kết nối dựa trên mô tả phân đoạn được cung cấp.
Điều này có thể được thực hiện theo nhiều cách khác nhau. Tôi đã làm điều đó bằng cách tạo đối tượng proxy đang chuyển các tên phân đoạn dưới dạng ActiveRecord
được ngụy trang các lớp học. Trình xử lý kết nối đang mong đợi nhận được mô hình AR và xem xét name
thuộc tính và cả ở superclass
để xem chuỗi mô hình phân cấp. Tôi đã triển khai DatabaseModel
lớp về cơ bản là tên phân đoạn, nhưng nó hoạt động giống như mô hình AR.
Triển khai
Đây là ví dụ thực hiện. Tôi đã sử dụng cơ sở dữ liệu sqlite vì đơn giản, bạn chỉ có thể chạy tệp này mà không cần bất kỳ thiết lập nào. Bạn cũng có thể xem qua ý chính này
# 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
Tôi nghĩ điều này sẽ đưa ra ý tưởng về cách triển khai giải pháp sẵn sàng sản xuất. Tôi hy vọng tôi đã không bỏ lỡ bất cứ điều gì rõ ràng ở đây. Tôi có thể đề xuất một số cách tiếp cận khác nhau:
- Lớp con
ActiveRecord::ConnectionAdapters::ConnectionHandler
và ghi đè những phương thức chịu trách nhiệm truy xuất nhóm kết nối - Tạo lớp hoàn toàn mới triển khai cùng một api với
ConnectionHandler
- Tôi đoán cũng có thể chỉ cần ghi đè
retrieve_connection
phương pháp. Tôi không nhớ nó được định nghĩa ở đâu, nhưng tôi nghĩ nó nằm trongActiveRecord::Core
.
Tôi nghĩ các phương pháp tiếp cận 1 và 2 là cách để đi và nên áp dụng cho tất cả các trường hợp khi làm việc với cơ sở dữ liệu.