Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 7: マルチプルDBのreading_request?がカスタマイズ可能になった(翻訳)

概要

同サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: 週刊Railsウォッチ20220510: マルチプルデータベースのリゾルバでreading_request?を定義

現時点の#44944mainブランチにのみ含まれています。

Rails 7: マルチプルDBのreading_request?がカスタマイズ可能になった(翻訳)

Rails 6でマルチプルデータベース接続のサポートが追加されたことで、読み出しと書き込みに別々のデータベースを設定可能になりました。

たとえば、以下のようにwrite_databaseread_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 7: バックグラウンドジョブで削除する最大レコード数を指定可能になった(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。