ravelll の日記

よしなに

Shopify/graphql-batch がどのように batching するのかを追う

この記事は GraphQL Advent Calendar 2020 4日目の記事です。

前回の記事は @qsona さんの 動的型付け言語での GraphQL Client でした。


題の通り、Shopify/graphql-batch がどのようにリクエストを batching するのかをコードを読みつつ追っていきます。

時間の都合で、この記事ではシンプルな単一の Query のみ(Mutation でない)を実行した場合を対象とします。また Loader については README に記載されている RecordLoader の実装をそのまま使います。

3行まとめ

  • graphql-ruby の lazy-loading class として Promise を使う
  • Loader.for(Klass).load(object.klass_id) が呼ばれるたびに Loader に Promise を登録する
  • Query にある遅延評価しない field を全て解決した後、Promise を解決する

コードリーディングに使ったサンプルコード

require 'bundler/inline'
require 'logger'

gemfile do
  source 'https://rubygems.org'
  gem 'graphql'
  gem 'graphql-batch'
  gem 'activerecord', require: 'active_record'
  gem 'sqlite3'
  # gem 'pry-byebug'
end

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :courses, force: true do |t|
    t.references :textbook
  end

  create_table :textbooks, force: true do |t|
  end
end

class Course < ActiveRecord::Base
  belongs_to :textbook
end

class Textbook < ActiveRecord::Base
  has_many :courses
end

textbook1 = Textbook.create!
textbook2 = Textbook.create!
textbook3 = Textbook.create!
Course.create!(textbook: textbook1)
Course.create!(textbook: textbook2)
Course.create!(textbook: textbook3)

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model)
    @model = model
  end

  def perform(ids)
    @model.where(id: ids).each { |record| fulfill(record.id, record) }
    ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
  end
end

class TextbookType < GraphQL::Schema::Object
  field :id, ID, null: false
end

class CourseType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :textbook, TextbookType, null: false
  def textbook
    RecordLoader.for(Textbook).load(object.textbook_id)
  end
end

class QueryType < GraphQL::Schema::Object
  field :courses, [CourseType], null: false
  def courses
    Course.all
  end
end

class MySchema < GraphQL::Schema
  query QueryType
  use GraphQL::Batch
end

result = MySchema.execute(<<~GQL)
  query Courses {
    courses {
      id
      textbook {
        id
      }
    }
  }
GQL

pp result.as_json

1. graphql-ruby の lazy-loading class として Promise を使う

2. Loader.for(Klass).load(object.klass_id) が呼ばれるたびに Loader に Promise を登録する

3. Query にある遅延評価しない field を全て解決した後、Promise を解決する

所感

思ったより理解に時間がかかり(そしてまだ十分に理解できていない)複雑度に対してだいぶラフなまとめになってしまったのですが、graphql-batch がどのように batching するか、また graphql-ruby についてもその実装の一部を理解することができ便利でした。

そしてコードリーディングってスコープを決めてやらないと一生終わらないですよね。楽しい。