カスタムローダーの作り方
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メソッドでのバッチ処理のロジックを拡張するとよいかと思います。