graphql-batchのカスタムローダーと作り方

カスタムローダーの作り方

graphql-batchのリポジトリに用意されているexamplesを参考に、カスタムローダーの作り方を見ていこうと思います。

まずLoaderと定義されているpublicメソッドを書き出してみます。

  • AssociationLoader
    • initialize
    • load
    • cache_key
    • perform
  • RecordLoader
    • initialize
    • load
    • perform
  • WindowKeyLoader
    • initialize
    • perform
  • HTTPLoader
    • initialize
    • perform
  • ActiveStorageLoader
    • initialize
    • perform

リファレンス実装されているどのLoaderでも共通して実装されているメソッドは、以下です。

  • initialize
  • perform

また、RecordLoaderでは、加えて「load」メソッド、AssociationLoaderでは加えて「load」、「cache_key」メソッドが実装されています。

共通で実装されるメソッドから役割や機能を見ていきましょう。

RecordLoaderを例にします。

initialize

initializeでは、graphql-rubyのfieldでLoaderを呼び出す際の、forメソッドに渡す引数を定義します。

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model, column: model.primary_key, where: nil)
    super()
    @model = model
    @column = column.to_s
    @column_type = model.type_for_attribute(@column)
    @where = where
  end

  ...
end
field :product, Types::Product, null: true do
  argument :id, ID, required: true
end

def product(id:)
  RecordLoader.for(Product).load(id)
end

perform

performでは、GraphQL resolverの解決後に遅延実行されるバッチ処理を記述します。 各fieldの呼び出し時にキャッシュしてあるkeyの一覧を受け取った上で実行される、バッチ処理のロジック定義します。 バッチ処理の実行後はキャッシュキーを使用して、Promiseをfulfillして完了させます。

class RecordLoader < GraphQL::Batch::Loader
  ...
  def perform(keys)
    query(keys).each { |record| fulfill(record.public_send(@column), record) }
    keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
  end
  ...
end

最低限、initializeとperofmメソッドを実装することでカスタムローダーが作成できますが、一部の例ではそれ以外のメソッドを実装しているので、そちらも見ていきたいと思います

load

cache keyへの登録処理をカスタマイズする場合に使用します。

RecordLoaderの場合、cache keyはそのまま、whereメソッドの特定のカラムの値になるため、loaderメソッドをoverrideして適切な型にあらかじめ変換しています。

...
def load(key)
  super(@column_type.cast(key))
end
...

AssociatoinLoaderの場合、対象となるモデルに指定されたassociationが存在するかどうかのチェックと、すでに対象のレコードのassociationが呼び出されている場合はcache keyへの登録はスキップするようになっています。

...
def load(record)
  raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
  return Promise.resolve(read_association(record)) if association_loaded?(record)
  super
end
...

cache_key

AssociatoinLoaderのみで実装されています。 cache keyをDBから取得したレコードのIDで設定すると、別々のAssociations間でcache keyが競合することから、ActiveRecordのレコードオブジェクトのIDをcache keyとして扱うようになっています。

# We want to load the associations on all records, even if they have the same id
def cache_key(record)
  record.object_id
end

まとめ

カスタムローダーを作成するにあたっての基本は - initializeメソッド - performメソッド を実装することが基本となります。

Initializeでバッチ処理対象のキーを受け取る、performでキャッシュされたキーを元にバッチ処理を実行し、Promiseを完了させます。

また、必要に応じてloadやcache_keyなどでキャッシュ対象のキーの前処理やcache_keyのカスタマイズなどを行う流れとなります。

実際にカスタムローダーを作成する場合は、N+1クエリの解消目的とすることが多いことから、まずはRecordLoaderやAssociationLoaderをベースにperformメソッドでのバッチ処理のロジックを拡張するとよいかと思います。

参考