隨著應用程式越來越受歡迎和使用,您需要擴充應用程式以支援新使用者和他們的資料。您的應用程式可能需要擴充的一個層面是在資料庫層級。Rails 支援使用多個資料庫,因此您不必將資料全部儲存在同一個地方。
目前支援下列功能
- 多個寫入資料庫和各個資料庫的複本
- 自動切換與您所使用模型的連線
- 根據 HTTP 動詞和最近的寫入內容,自動在寫入資料庫和複本之間切換
- Rails 任務,用於建立、刪除、遷移和與多個資料庫互動
下列功能(目前)不支援
- 負載平衡複本
1 設定您的應用程式
雖然 Rails 會試著為您完成大部分的工作,但您仍需要執行一些步驟,才能讓您的應用程式準備好使用多個資料庫。
假設我們有一個應用程式,其中只有一個寫入資料庫,而且我們需要為一些新加入的資料表新增一個新的資料庫。新資料庫的名稱會是「animals」。
database.yml
如下所示
production:
database: my_primary_database
adapter: mysql2
username: root
password: <%= ENV['ROOT_PASSWORD'] %>
讓我們新增一個名為 animals 的第二個資料庫,以及兩個資料庫的複本。為此,我們需要將我們的 database.yml
從 2 層式設定變更為 3 層式設定。
如果提供了主要設定,它將會用作「預設」設定。如果沒有名為 "primary"
的設定,Rails 會將第一個設定用作每個環境的預設設定。預設設定會使用預設的 Rails 檔名。例如,主要設定會使用 schema.rb
作為架構檔,而所有其他項目會使用 [CONFIGURATION_NAMESPACE]_schema.rb
作為檔名。
production:
primary:
database: my_primary_database
username: root
password: <%= ENV['ROOT_PASSWORD'] %>
adapter: mysql2
primary_replica:
database: my_primary_database
username: root_readonly
password: <%= ENV['ROOT_READONLY_PASSWORD'] %>
adapter: mysql2
replica: true
animals:
database: my_animals_database
username: animals_root
password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>
adapter: mysql2
migrations_paths: db/animals_migrate
animals_replica:
database: my_animals_database
username: animals_readonly
password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %>
adapter: mysql2
replica: true
在使用多個資料庫時,有一些重要的設定。
首先,primary
和 primary_replica
的資料庫名稱應該相同,因為它們包含相同的資料。animals
和 animals_replica
也是如此。
其次,寫入者和副本的使用者名稱應不同,並且副本使用者的資料庫權限應設定為僅讀取,而不寫入。
使用副本資料庫時,您需要在 database.yml
中為副本新增 replica: true
項目。這是因為 Rails 否則無法知道哪一個是副本,哪一個是寫入者。Rails 將不會對副本執行某些任務,例如遷移。
最後,對於新的寫入者資料庫,您需要將 migrations_paths
設定為您將儲存該資料庫遷移的目錄。我們稍後將在這個指南中進一步了解 migrations_paths
。
現在我們有了新的資料庫,讓我們設定連線模型。為了使用新的資料庫,我們需要建立一個新的抽象類別並連接到 animals 資料庫。
class AnimalsRecord < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :animals, reading: :animals_replica }
end
然後,我們需要更新 ApplicationRecord
以了解我們的新的副本。
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary_replica }
end
如果您為應用程式記錄使用不同名稱的類別,則需要設定 primary_abstract_class
,以便 Rails 知道 ActiveRecord::Base
應與哪個類別共用連線。
class PrimaryApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
連接到 primary/primary_replica 的類別可以繼承自您的主要抽象類別,就像標準 Rails 應用程式一樣
class Person < ApplicationRecord
end
預設情況下,Rails 預期資料庫角色分別為 primary 和 replica 的 writing
和 reading
。如果您有舊系統,您可能已經設定了不想變更的角色。在這種情況下,您可以在應用程式設定中設定新的角色名稱。
config.active_record.writing_role = :default
config.active_record.reading_role = :readonly
在單一模型中連接到資料庫,然後繼承該模型以取得資料表,而不是將多個個別模型連接到同一個資料庫,這一點很重要。資料庫用戶端對可開啟的連線數量有限制,如果您這樣做,它會增加連線數量,因為 Rails 使用模型類別名稱作為連線規範名稱。
現在我們有了 database.yml
和設定好的新模型,是時候建立資料庫了。Rails 6.0 內建所有您在 Rails 中使用多個資料庫所需的 rails 任務。
您可以執行 bin/rails -T
以查看所有您可以執行的指令。您應該會看到下列內容
$ bin/rails -T
bin/rails db:create # Create the database from DATABASE_URL or config/database.yml for the ...
bin/rails db:create:animals # Create animals database for current environment
bin/rails db:create:primary # Create primary database for current environment
bin/rails db:drop # Drop the database from DATABASE_URL or config/database.yml for the cu...
bin/rails db:drop:animals # Drop animals database for current environment
bin/rails db:drop:primary # Drop primary database for current environment
bin/rails db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
bin/rails db:migrate:animals # Migrate animals database for current environment
bin/rails db:migrate:primary # Migrate primary database for current environment
bin/rails db:migrate:status # Display status of migrations
bin/rails db:migrate:status:animals # Display status of migrations for animals database
bin/rails db:migrate:status:primary # Display status of migrations for primary database
bin/rails db:reset # Drop and recreates all databases from their schema for the current environment and loads the seeds
bin/rails db:reset:animals # Drop and recreates the animals database from its schema for the current environment and loads the seeds
bin/rails db:reset:primary # Drop and recreates the primary database from its schema for the current environment and loads the seeds
bin/rails db:rollback # Roll the schema back to the previous version (specify steps w/ STEP=n)
bin/rails db:rollback:animals # Rollback animals database for current environment (specify steps w/ STEP=n)
bin/rails db:rollback:primary # Rollback primary database for current environment (specify steps w/ STEP=n)
bin/rails db:schema:dump # Create a database schema file (either db/schema.rb or db/structure.sql ...
bin/rails db:schema:dump:animals # Create a database schema file (either db/schema.rb or db/structure.sql ...
bin/rails db:schema:dump:primary # Create a db/schema.rb file that is portable against any DB supported ...
bin/rails db:schema:load # Load a database schema file (either db/schema.rb or db/structure.sql ...
bin/rails db:schema:load:animals # Load a database schema file (either db/schema.rb or db/structure.sql ...
bin/rails db:schema:load:primary # Load a database schema file (either db/schema.rb or db/structure.sql ...
bin/rails db:setup # Create all databases, loads all schemas, and initializes with the seed data (use db:reset to also drop all databases first)
bin/rails db:setup:animals # Create the animals database, loads the schema, and initializes with the seed data (use db:reset:animals to also drop the database first)
bin/rails db:setup:primary # Create the primary database, loads the schema, and initializes with the seed data (use db:reset:primary to also drop the database first)
執行像 bin/rails db:create
的指令會建立主要和 animals 資料庫。請注意,沒有建立資料庫使用者的指令,您需要手動執行此操作,以支援複製品的唯讀使用者。如果您只想建立 animals 資料庫,您可以執行 bin/rails db:create:animals
。
2 連接到資料庫而不管理架構和遷移
如果您想連接到外部資料庫,而不執行任何資料庫管理任務,例如架構管理、遷移、種子等,您可以設定每個資料庫設定選項 database_tasks: false
。預設設定為 true。
production:
primary:
database: my_database
adapter: mysql2
animals:
database: my_animals_database
adapter: mysql2
database_tasks: false
3 產生器和遷移
多個資料庫的遷移應存放在自己的資料夾中,資料夾名稱加上設定中資料庫金鑰的前綴。
您還需要在資料庫設定中設定 migrations_paths
,以告訴 Rails 在哪裡尋找遷移。
例如,animals
資料庫會在 db/animals_migrate
目錄中尋找遷移,而 primary
會在 db/migrate
中尋找。Rails 產生器現在採用 --database
選項,以便在正確的目錄中產生檔案。指令可以這樣執行
$ bin/rails generate migration CreateDogs name:string --database animals
如果您使用 Rails 產生器,則架構和模型產生器會為您建立抽象類別。只要傳遞資料庫金鑰至命令列即可。
$ bin/rails generate scaffold Dog name:string --database animals
會建立一個具有資料庫名稱和 Record
的類別。在此範例中,資料庫為 Animals
,因此我們會得到 AnimalsRecord
class AnimalsRecord < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :animals }
end
產生的模型會自動繼承自 AnimalsRecord
。
class Dog < AnimalsRecord
end
由於 Rails 不知道哪個資料庫是您的寫入器副本,因此您需要在完成後將其新增至抽象類別。
Rails 只會產生一次新類別。它不會被新的架構覆寫,也不會在刪除架構時被刪除。
如果您已經有抽象類別,且其名稱與 AnimalsRecord
不同,您可以傳遞 --parent
選項,以指出您要使用不同的抽象類別
$ bin/rails generate scaffold Dog name:string --database animals --parent Animals::Record
這會略過產生 AnimalsRecord
,因為您已向 Rails 指出您要使用不同的父類別。
4 啟用自動角色切換
最後,為了在您的應用程式中使用唯讀副本,您需要啟用自動切換的中介軟體。
自動切換允許應用程式根據 HTTP 動詞以及請求使用者最近是否有寫入,在寫入器和副本之間切換。
如果應用程式收到 POST、PUT、DELETE 或 PATCH 請求,則應用程式會自動寫入寫入器資料庫。如果請求不是這些方法之一,但應用程式最近進行了寫入,則也會使用寫入器資料庫。所有其他請求都會使用副本資料庫。
若要啟用自動連線切換中介軟體,您可以執行自動交換產生器
$ bin/rails g active_record:multi_db
然後取消註解下列各行
Rails.application.configure do
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
Rails 保證「讀取自己的寫入」,如果在 delay
視窗內,它會將 GET 或 HEAD 要求傳送給寫入器。預設延遲設定為 2 秒。您應該根據資料庫基礎架構變更此設定。在延遲視窗內,Rails 不保證「讀取最近寫入」給其他使用者,而且會將 GET 和 HEAD 要求傳送給複製品,除非他們最近有寫入。
Rails 中的自動連線切換相對原始,而且故意不執行太多動作。目標是建立一個系統,展示如何執行自動連線切換,而且應用程式開發人員可以彈性自訂。
Rails 中的設定讓您可以輕鬆變更切換方式,以及它所根據的參數。假設您想使用 cookie 而不是工作階段來決定何時切換連線。您可以撰寫自己的類別
class MyCookieResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
def self.call(request)
new(request.cookies)
end
def initialize(cookies)
@cookies = cookies
end
attr_reader :cookies
def last_write_timestamp
self.class.convert_timestamp_to_time(cookies[:last_write])
end
def update_last_write_timestamp
cookies[:last_write] = self.class.convert_time_to_timestamp(Time.now)
end
def save(response)
end
end
然後將它傳遞給中間軟體
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = MyCookieResolver
5 使用手動連線切換
在某些情況下,您可能希望您的應用程式連線到寫入器或複製品,而自動連線切換並不夠用。例如,您可能知道對於特定要求,您總是想要將要求傳送給複製品,即使您在 POST 要求路徑中也是如此。
為此,Rails 提供了一個 connected_to
方法,它會切換到您需要的連線。
ActiveRecord::Base.connected_to(role: :reading) do
# all code in this block will be connected to the reading role
end
connected_to
呼叫中的「角色」會查詢在該連線處理常式(或角色)上連線的連線。reading
連線處理常式會保留所有透過 connects_to
連線的連線,其角色名稱為 reading
。
請注意,具有角色的 connected_to
會查詢現有的連線,並使用連線規格名稱進行切換。這表示如果您傳遞未知的角色,例如 connected_to(role: :nonexistent)
,您會收到錯誤訊息,指出 ActiveRecord::ConnectionNotEstablished (找不到 'ActiveRecord::Base' 的連線池,用於 'nonexistent' 角色。)
如果您希望 Rails 確保執行任何查詢都是唯讀的,請傳遞 prevent_writes: true
。這只是防止看起來像寫入的查詢被傳送到資料庫。您還應該將您的複本資料庫設定為以唯讀模式執行。
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
# Rails will check each query to ensure it's a read query
end
6 水平分片
水平分片是指您將資料庫拆分以減少每個資料庫伺服器上的列數,但維護「分片」之間相同的架構。這通常稱為「多租戶」分片。
在 Rails 中支援水平分片的 API 類似於自 Rails 6.0 以來就存在的多個資料庫/垂直分片 API。
分片在三層配置中宣告如下
production:
primary:
database: my_primary_database
adapter: mysql2
primary_replica:
database: my_primary_database
adapter: mysql2
replica: true
primary_shard_one:
database: my_primary_shard_one
adapter: mysql2
migrations_paths: db/migrate_shards
primary_shard_one_replica:
database: my_primary_shard_one
adapter: mysql2
replica: true
migrations_paths: db/migrate_shards
primary_shard_two:
database: my_primary_shard_two
adapter: mysql2
migrations_paths: db/migrate_shards
primary_shard_two_replica:
database: my_primary_shard_two
adapter: mysql2
replica: true
migrations_paths: db/migrate_shards
然後透過 shards
鍵使用 connects_to
API 將模型連接起來
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
connects_to database: { writing: :primary, reading: :primary_replica }
end
class ShardRecord < ApplicationRecord
self.abstract_class = true
connects_to shards: {
shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica },
shard_two: { writing: :primary_shard_two, reading: :primary_shard_two_replica }
}
end
如果您使用分片,請務必將 migrations_paths
設定為所有分片的相同路徑。在產生遷移時,您可以傳遞 --database
選項並使用其中一個分片名稱。由於它們都設定相同的路徑,因此您選擇哪一個並不重要。
$ bin/rails g scaffold Dog name:string --database primary_shard_one
然後,模型可以透過 connected_to
API 手動交換分片。如果使用分片,則必須傳遞 role
和 shard
ActiveRecord::Base.connected_to(role: :writing, shard: :default) do
@id = Person.create! # Creates a record in shard named ":default"
end
ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do
Person.find(@id) # Can't find record, doesn't exist because it was created
# in the shard named ":default".
end
水平分片 API 也支援讀取複本。您可以使用 connected_to
API 交換角色和分片。
ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one) do
Person.first # Lookup record from read replica of shard one
end
7 啟用自動分片切換
應用程式可以使用提供的中間件在每個請求中自動切換分片。
ShardSelector
中間件提供了一個自動交換分片的架構。Rails 提供了一個基本架構來確定要切換到哪個分片,並允許應用程式在需要時撰寫自訂交換策略。
ShardSelector
會採用一組選項(目前僅支援 lock
),中介軟體可以使用這些選項來變更行為。lock
預設為 true,且會禁止要求在區塊內切換分片。如果 lock
為 false,則允許切換分片。對於基於租戶的分片,lock
應始終為 true,以防止應用程式程式碼錯誤地在租戶之間切換。
可以與資料庫選擇器相同的產生器用於產生自動分片切換的檔案
$ bin/rails g active_record:multi_db
然後在檔案中取消註解下列內容
Rails.application.configure do
config.active_record.shard_selector = { lock: true }
config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard }
end
應用程式必須提供解析器的程式碼,因為它取決於應用程式特定的模型。範例解析器如下所示
config.active_record.shard_resolver = ->(request) {
subdomain = request.subdomain
tenant = Tenant.find_by_subdomain!(subdomain)
tenant.shard
}
8 細粒度的資料庫連線切換
在 Rails 6.1 中,可以切換一個資料庫的連線,而不是所有資料庫的連線。
透過細粒度的資料庫連線切換,任何抽象連線類別都可以在不影響其他連線的情況下切換連線。這對於將 AnimalsRecord
查詢切換為從副本讀取,同時確保 ApplicationRecord
查詢轉到主資料庫很有用。
AnimalsRecord.connected_to(role: :reading) do
Dog.first # Reads from animals_replica
Person.first # Reads from primary
end
也可以針對分片細粒度地切換連線。
AnimalsRecord.connected_to(role: :reading, shard: :shard_one) do
Dog.first # Will read from shard_one_replica. If no connection exists for shard_one_replica,
# a ConnectionNotEstablished error will be raised
Person.first # Will read from primary writer
end
若要僅切換主資料庫叢集,請使用 ApplicationRecord
ApplicationRecord.connected_to(role: :reading, shard: :shard_one) do
Person.first # Reads from primary_shard_one_replica
Dog.first # Reads from animals_primary
end
ActiveRecord::Base.connected_to
保留了全球切換連線的能力。
8.1 處理跨資料庫聯結的關聯
從 Rails 7.0+ 開始,Active Record 有個選項可以處理會在多個資料庫中執行聯結的關聯。如果您有想要停用聯結並執行 2 個或更多查詢的 has many through 或 has one through 關聯,請傳遞 disable_joins: true
選項。
例如
class Dog < AnimalsRecord
has_many :treats, through: :humans, disable_joins: true
has_many :humans
has_one :home
has_one :yard, through: :home, disable_joins: true
end
class Home
belongs_to :dog
has_one :yard
end
class Yard
belongs_to :home
end
先前在沒有 disable_joins
的情況下呼叫 @dog.treats
或沒有 disable_joins
的情況下呼叫 @dog.yard
會引發錯誤,因為資料庫無法處理跨叢集的聯結。使用 disable_joins
選項後,Rails 會產生多個選取查詢,以避免嘗試跨叢集聯結。對於上述關聯,@dog.treats
會產生下列 SQL
SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ? [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?) [["human_id", 1], ["human_id", 2], ["human_id", 3]]
而 @dog.yard
會產生下列 SQL
SELECT "home"."id" FROM "homes" WHERE "homes"."dog_id" = ? [["dog_id", 1]]
SELECT "yards".* FROM "yards" WHERE "yards"."home_id" = ? [["home_id", 1]]
使用此選項時,有一些重要事項需要注意
- 由於現在會執行兩個或更多查詢(視關聯而定)而非聯結,因此可能會影響效能。如果
humans
的選取傳回大量的 ID,則treats
的選取可能會傳送過多的 ID。 - 由於我們不再執行聯結,因此包含順序或限制的查詢現在會在記憶體中排序,因為無法將一個表格的順序套用至另一個表格。
- 必須將此設定新增至所有要停用聯結的關聯。Rails 無法為您猜測此設定,因為關聯載入是延遲的,Rails 在
@dog.treats
中載入treats
時,已經需要知道應該產生哪些 SQL。
8.2 架構快取
如果您要為每個資料庫載入架構快取,則必須在每個資料庫設定中設定 schema_cache_path
,並在應用程式設定中設定 config.active_record.lazily_load_schema_cache = true
。請注意,這會在建立資料庫連線時延遲載入快取。
9 注意事項
9.1 負載平衡複本
Rails 也不支援複本的自動負載平衡。這非常仰賴您的基礎架構。我們可能會在未來實作基本的原始負載平衡,但對於大規模的應用程式,這應該是您的應用程式在 Rails 外部處理的事項。
回饋
我們鼓勵您協助提升本指南的品質。
如果你發現任何錯字或事實錯誤,請貢獻你的力量。首先,你可以閱讀我們的文件貢獻章節。
你可能也會發現不完整或未更新的內容。請為 main 新增任何遺失的文件。請務必先查看Edge Guides,以驗證問題是否已在 main 分支上修復。查看Ruby on Rails 指南方針,了解樣式和慣例。
如果你發現需要修復的地方,但無法自行修補,請開啟問題。
最後但並非最不重要的是,歡迎在官方 Ruby on Rails 論壇上討論任何與 Ruby on Rails 文件相關的事項。