🦔
graphql-rubyで、queryだけのリクエストの場合リードレプリカに接続する
Railsのマルチデータベース機能を利用しWriterインスタンスとReaderインスタンスを分けて利用しているRailsプロジェクトで、リクエストのGraphQLクエリがデータ取得しか行わないのであればReaderインスタンスに自動で接続するようにしたいです。
Why
- Writerインスタンスの負荷軽減のため、Readerインスタンスを可能な限り利用したい。
-
ActiveRecord::Base.connected_to(role: :reading)
を利用せず、自動で機械的にDBロールを切り替えたい。- 利用者の裁量に委ねるとなると切り替えの漏れが発生するため
解決策
実装
module Tracers
class DatabaseRoleSelector
EVENT_NAME = 'execute_multiplex'.freeze
# @param [String] event
# @param [Hash] data
# @option data [GraphQL::Execution::Multiplex] multiplex
def trace(event, data, &block)
if event == EVENT_NAME
multiplex = data[:multiplex]
role = if multiplex.queries.all?(&:query?)
ActiveRecord::Base.reading_role
else
ActiveRecord::Base.writing_role
end
Rails.logger.debug("[#{self.class.name}] ActiveRecord::Base.connected_to(role: #{role.inspect})")
ActiveRecord::Base.connected_to(role: role, &block)
else
yield
end
end
end
end
# app/graphql/my_schema.rb
class MySchema < GraphQL::Schema
tracer Tracers::DatabaseRoleSelector.new
end
解説
Trancers
graphql–rubyのtracingという仕組みを利用します。
これはGraphQLクエリに対してrackミドルウェア的にコードを挿入する仕組みで、trace
メソッドを実装し、処理を追加した後 yield
を呼び出すことで、クエリに対して前処理を追加できます。
Datadog等の監視プラットフォームからクエリの追跡に使われることが多いです。
event
はイベント名を表します。一覧はこちらです: https://graphql-ruby.org/api-doc/2.0.9/GraphQL/Tracing
data
は後述します。
multiplex
dataハッシュの中のmultiplexを利用します。
multiplexは、Clientから複数のクエリがバッチ的にリクエストされた場合でも処理を行えるような仕組みです。
とはいえ、GraphQLの全てのクエリはmultiplexとして実装されています。バッチを意図していなくとも、1つの要素を含む配列として渡ってきます。
database role
database roleの判定ロジックは、multiplexオブジェクトのqueriesが全てクエリである(== readのみを行うクエリである)場合はReaderインスタンスに接続します。
まだ検討していないこと
queryだがDBのwriteがしたい。迂回できないか?
- write処理は本当にqueryで必要ですか?mutationに分割できませんか?
- 必要になったタイミングで考えるのが良さそうです。迂回できるように実装してください。
DB分割をするなど、違うDBインスタンスを見る必要が出てきた
- 何かしらのフラグを見てどのDBに接続するか判定するロジックを追加するのが良さそうです。
参考
- マルチデータベースのサポートについて議論されているIssueです。今回の実装の参考にさせていただきました。
- マルチデータベースのサポートをgraphql-ruby側で取り込むことは考えておらず、利用者側でオプトインしてほしいとのことでした。
Discussion