0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQL Ruby で Aurora のリードレプリカにクエリを向ける

Posted at

はじめに

GraphQL で RDS のリードレプリカを使用することで、読み取り専用クエリの負荷を分散させることができます。本記事では、Rails + GraphQL-Ruby でリードレプリカにクエリをルーティングする実装方法と、注意すべきポイントについてまとめます。

アーキテクチャ概要

GraphQL のクエリ実行時に、読み取り専用クエリかどうかを判定し、自動的にリードレプリカまたはライターに接続を切り替えます。

GraphQL Query
    ↓
DatabaseConnectionTracer (クエリ種別の判定)
    ↓
readonly? → リードレプリカ (primary_replica)
mutation? → ライター (primary)

実装方法

1. データベース設定

まず、config/database.ymlでリードレプリカの設定を行います。

production:
  primary:
    <<: *default
  primary_replica:
    <<: *default
    host: <%= ENV["GIFT_WALLET_NEO_DATABASE_READER_HOST"] %>
    replica: true

2. DatabaseConnectionTracer

GraphQL の Tracer を使って、クエリ実行時にデータベース接続を自動的に切り替えます。

backend/app/graphql/tracers/database_connection_tracer.rb
module Tracers
  module DatabaseConnectionTracer
    EVENT_NAME = "execute_multiplex".freeze

    def self.trace(event, data)
      if event == EVENT_NAME
        multiplex = data[:multiplex]
        read_query = multiplex.queries.all?(&:query?)

        if read_query
          ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
            yield
          end
        else
          ActiveRecord::Base.connected_to(role: :writing) do
            yield
          end
        end
      else
        yield
      end
    end
  end
end

この Tracer は以下のロジックで動作します:

  1. execute_multiplexイベントをフック
  2. すべてのクエリが読み取り専用(query?true)の場合、ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true)でリードレプリカに接続
  3. 1 つでもミューテーションが含まれる場合、ActiveRecord::Base.connected_to(role: :writing)でライターに接続

3. GraphQL スキーマへの適用

スキーマに Tracer を登録します。

backend/app/graphql/internal_api_schema.rb
class InternalAPISchema < GraphQL::Schema
  # ... その他の設定 ...

  trace_with Tracers::DatabaseConnectionTracer

  use GraphQL::Dataloader
end

正しい使い方

Dataloader を使った関連データのロード

GraphQL で関連データをロードする際は、Dataloader のAssociationPreloaderを使用します。これにより、N+1 問題を回避しつつ、親のクエリが読み取り専用の場合、リードレプリカに接続されます。

backend/app/graphql/types/internal_api/user_type.rb
module Types::InternalAPI
  class UserType < Types::BaseObject
    field :profile, User::ProfileType, null: true

    def profile
      dataloader.with(Loaders::AssociationPreloader, ::User, :profile).load(object)
    end
  end
end

この実装では、UserTypeprofileフィールドを解決する際に、親のクエリが読み取り専用であれば、リードレプリカからデータを取得します。

やってはいけない方法

❌ Type レベルで readonly を設定する方法

以下のように、Type のメソッド内で直接readonlyを呼び出すのは推奨されません

# ❌ 悪い例
module Types::InternalAPI
  class UserType < Types::BaseObject
    field :profile, User::ProfileType, null: true

    def profile
      ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
        # この方法では、loader内のクエリがwriterに向けられる可能性がある
        dataloader.with(Loaders::AssociationPreloader, ::User, :profile).load(object)
      end
    end
  end
end

問題点:

  1. Loader の関連先が writer にクエリされる

    • DatabaseConnectionTracerが全体のクエリをreadonlyブロックでラップしているにもかかわらず、Type 内で再度readonlyを設定すると、loader 内のデータベースクエリが意図しない接続先(writer)に向けられる可能性があります
    • GraphQL::Dataloader の実行コンテキストと、ActiveRecord の接続管理が競合する可能性があります
  2. 接続状態の不整合

    • 親のクエリがすでにreadonlyブロック内で実行されている場合、Type 内で再度readonlyを設定すると、接続状態が不整合になる可能性があります

✅ 正しい方法

Tracer レベルで一括管理し、Type 内では Dataloader をそのまま使用します。

# ✅ 良い例
module Types::InternalAPI
  class UserType < Types::BaseObject
    field :profile, User::ProfileType, null: true

    def profile
      # Tracerが全体をreadonlyでラップしているので、そのまま使用する
      dataloader.with(Loaders::AssociationPreloader, ::User, :profile).load(object)
    end
  end
end

まとめ

  • GraphQL の Tracer を使って、クエリ実行時に自動的にリードレプリカまたはライターに接続を切り替える
  • Type レベルで個別にreadonlyを設定するのは避ける(loader 内のクエリが writer に向けられる可能性がある)
  • Dataloader を使用する際は、Tracer レベルでの接続管理に任せる
  • エラーハンドリングを適切に行い、リーダーのスケールインなどによる接続エラーに対応する

この実装により、読み取り専用クエリの負荷をリードレプリカに分散させ、パフォーマンスを向上やリーダーのスケールアウトをさせることができます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?