Rails 7: マルチプルDBのreading_request?がカスタマイズ可能になった(翻訳)
Rails 6でマルチプルデータベース接続のサポートが追加されたことで、読み出しと書き込みに別々のデータベースを設定可能になりました。
たとえば、以下のようにwrite_database
とread_database
という2つのデータベースがあるとします。
# /config/database.yml
default: &default
adapter: postgresql
encoding: unicode
host: <%= ENV['PG_HOST'] || 'localhost' %>
pool: 5
username: <%= ENV['PG_USER'] || 'postgres' %>
password: <%= ENV['PG_PASSWORD'] || 'postgres' %>
development:
write_database:
<<: *default
database: <%= "write_database" %>
read_database:
<<: *default
database: <%= "read_database" %>
上で定義したデータベースを使うには、Active Recordモデルを以下のようにセットアップする必要があります。
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :write_database, reading: :read_database }
end
# app/models/product.rb
class Product < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
end
HTTPメソッドに応じて2つのデータベースを自動的に切り替えるには、以下のコンフィグを追加する必要があります。
# config/application.rb
module MultiDBApp
class Application < Rails::Application
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
end
上のコンフィグを使うと、RailsはActiveRecord::Middleware::Middleware::DatabaseSelector
ミドルウェア内にreading_request?
メソッドが定義されているかどうかを調べます。デフォルトの実装では、GETリクエストとHEADリクエストについてtrueになります。つまりPOST・PUT・DELETE・PATCHリクエストについては、アプリケーションが自動的に書き込み先をwrite_database
に設定し、読み取りにはread_database
を使うようになります。
原注
Railsガイドによると、上記のコンフィグはイニシャライザ(/config/initializers/multi_db.rb)に追加することになっていますが、最新リリースのRailsでは動かないので、Rails issue #45162のコメントで提案されているように、コンフィグをapplication.rbに追加する必要があります。
改修前
create_product
(POST)APIを呼んでProductを作成してみましょう。
POST:- http://localhost:3000/products
Body:- { name: "Detergent", description: "A mixture of surfactants with cleansing properties" }
Response:-
{
"id": 1,
"name": "Detergent",
"description": "A mixture of surfactants with cleansing properties",
"created_at": "2022-06-14T06:25:25.877Z",
"updated_at": "2022-06-14T06:25:25.877Z"
}
上のように、このproductが期待どおりwrite_database
で作成されます。
ここでget_product
(GET)APIを呼び出してみると、GETリクエストがread_database
にリダイレクトされるので、以下のようにproductが見つからないというレスポンスが返され、さっき作ったproductがread_database
に存在していないことがわかります。
GET:- http://localhost:3000/products/1
Response:-
{
"error": "product with id 1 not found"
}
この動作は期待どおりです。
今度は、GraphQL APIで特定のproductを読み出す場合を考えてみましょう。
GraphQL API:-
{
product(id: 1) {
id
name
description
}
}
Response:-
{
"data": {
"product": {
"id": "1",
"title": "Detergent",
"description": "A mixture of surfactants with cleansing properties"
}
}
}
困ったことに、読み出せてはいけないはずのproductの詳細情報を取得できてしまいました。理想的には読み取りリクエストに対してnot_found
を返すべきなのですが、GraphQL APIはHTTPリクエストにPOSTメソッドを使うので、デフォルトの実装ではPOSTリクエストがwrite_database
にリダイレクトされます。
改修後
この問題を修正するため、RailsのActiveRecord::Middleware::Middleware::DatabaseSelector
クラスにあったreading_request?
メソッドがActiveRecord::Middleware::DatabaseSelector::Resolver
クラスに移動し、カスタムリゾルバを作成してこのメソッドをオーバーライド可能になりました。
reading_request?
メソッドをオーバーライドするカスタムリゾルバを作成して、GraphQL APIのバリデーションを追加してみましょう。
class CustomResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
def reading_request?(request)
graphql_read = request.post? && request.path == "/graphql" && !request.params[:query]&.include?("mutation")
graphql_read || super
end
end
module MultiDBApp
class Application < Rails::Application
config.load_defaults 7.1
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = CustomResolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
end
これで、カスタマイズしたreading_request?
によって「リクエストがPOSTかどうか」「種別がGraphQLかどうか」「パラメータにmutation
が含まれていないかどうか」がチェックされるようになり、読み取りリクエストとみなしてread_database
にリダイレクトするようになります。
それでは、GraphQL APIを再度呼び出して特定のproductを読み取ってみましょう。
GraphQL API:-
{
product(id: 1) {
id
name
description
}
}
Response:-
{
"errors": [
{
"message": "Couldn't find Product with 'id'=1"
}
]
}
read_database
にはProductのレコードがないので、期待どおりnot_found
メッセージが返されます。
原注: この改修は、まだ公式バージョンのRailsではリリースされていません。
詳しくは#44944を参照してください。
概要
同サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
参考: 週刊Railsウォッチ20220510: マルチプルデータベースのリゾルバで
reading_request?
を定義現時点の#44944はmainブランチにのみ含まれています。