🦔
N+1の解消だけじゃない。GraphQL::Batchを使ってfieldの並列取得をサクッと実装する
GraphQLで発生するN+1問題を解消するために、GraphQL::Batchを使っていたのですが、GraphQL::BatchはN+1解消だけではなく、fieldの並列取得にも使えることに気づいたので検証してみました。
なお、GraphQL::Batchを使ってN+1を解消する方法は別途記事を書いているので興味がある方はこちらをどうぞ!
検証
取得に5秒かかるフィールドを2つ準備します。
app/models/user.rb
class User < ApplicationRecord
def slow_field
5.times.each do
Rails.logger.info('slow_field')
sleep(1)
end
'slow_field'
end
def slow_field2
5.times.each do
Rails.logger.info('slow_field2')
sleep(1)
end
'slow_field2'
end
end
上記フィールドを取得するuser
クエリーを実装します。
app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :user, Types::UserType, null: true do
argument :id, Int, required: true
end
def user(id:)
User.find id
end
end
end
app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :slow_field, String, null: false
field :slow_field2, String, null: false
end
end
下記のクエリーを実行します。
query User($id: Int!) {
user(id: $id) {
slowField
slowField2
}
}
クエリーを実行すると実行ログは下記のようになります。
slow_field
slow_field
slow_field
slow_field
slow_field
slow_field2
slow_field2
slow_field2
slow_field2
slow_field2
Completed 200 OK in 10120ms (Views: 1.4ms | ActiveRecord: 1.1ms | Allocations: 9342)
ログからslowField
とslowField2
を直列で取得していることがわかります。
それぞれ5秒かかるので返却されるまで10秒ほどかかっています。
並列実行
並列実行できるようにローダーを追加します。
app/loader/promise_loader.rb
class PromiseLoader < GraphQL::Batch::Loader
def initialize(model)
super()
@model = model
end
def perform(methods)
futures = methods.map do |method|
Concurrent::Promises.future { @model.public_send(method) }
end
methods.each_with_index.each do |method, index|
fulfill(method, futures[index].value)
end
end
end
UserType
でPromiseLoader
を使うようにします。
app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :slow_field, String, null: false
field :slow_field2, String, null: false
def slow_field
PromiseLoader.for(object).load(:slow_field)
end
def slow_field2
PromiseLoader.for(object).load(:slow_field2)
end
end
end
この状態で先ほどと同じクエリーを実行すると下記のようにログ出力されます。
slow_field
slow_field2
slow_field
slow_field2
slow_field
slow_field2
slow_field
slow_field2
slow_field
slow_field2
Completed 200 OK in 5036ms (Views: 1.1ms | ActiveRecord: 0.9ms | Allocations: 1439)
ログからslowField
とslowField2
を並列で取得していることがわかります。
それぞれ5秒かかる処理ですが、並列で取得しているので5秒ほどで取得できています。
Discussion