graphql-batchのloaderをコード量1/9で使う方法
はじめに
僕が勤めている会社(株式会社マイベスト)では、Railsを使いながらGraphQL APIを実装しています。
ライブラリとしてgraphql-rubyを使っています。
(この記事の内容は、graphql-rubyが前提になります)
そして、N+1対策のため、graphql-batchも使っています。
今回はgraphql-batchを使った実装で、コード量を1/9に減らすことができた話をします。
graphql-batchとは
graphql-batchとはShopifyが開発しているgemで、graphql-rubyを使ったGraphQL APIにおいて、N+1を防ぐように関連データをロードできるようにするためのものです。
graphql-batchについての詳細はココでは割愛します。
ググってもらえれば色々と情報は出てくると思うので、そちらを参考にしてみてください 🙏
標準的な実装
graphql-batchを使うと、ObjectTypeの中身は以下のようなコードになります。
(gemのexampleに書いているAssociationLoaderを使う想定です)
module ObjectTypes
class ProductType < BaseObject
field :shops, [ObjectTypes::ShopType], null: false
def shops
Loaders::AssociationLoader.for(object.class, :shops).load(object)
end
end
end
・・・関連テーブルのfiled全てに↑のようなボイラープレートを書かないといけないのは手間ですよね。
今回修正した結果
そこで、graphql-rubyの仕組みを利用して、fieldのオプションを指定するだけでloaderを使えるようにしました。
修正後のコードがこちら
field :products, [ObjectTypes::ProductType], null: false, load: true
loaderに関係する箇所はload: true
だけなので、かなりスッキリしました。
文字数で言うと(スペース抜きで)81文字が9文字になっているので、9分の1になりました!
実装方法
方針
先ほど見た、標準的な実装例のresolverメソッドをよく見ると、shops
さえ指定できれば、他は共通でいけそうです
def shops
Loaders::AssociationLoader.for(object.class, :shops).load(object)
end
shops
(=field名)を使って共通のresolverを書けるようにしていきましょう。
field拡張
まずは、graphql-rubyのドキュメントに則ってfieldを拡張します
loadという引数を追加してbooleanを渡せるようにします
class Types::LoadableField < GraphQL::Schema::Field
def initialize(*args, load: false, **kwargs, &block)
super(*args, **kwargs, &block)
end
end
resolverメソッド
拡張したfieldの中で、loadがtrueならload_assocというメソッドを呼び出すようにresolver指定します
load_assocは後ほど定義します
class Types::LoadableField < GraphQL::Schema::Field
def initialize(*args, load: false, **kwargs, &block)
+ if load
+ super(*args, resolver_method: :load_assoc, extras: [:graphql_name], **kwargs, &block)
+ else
super(*args, **kwargs, &block)
+ end
end
end
- extrasは、resolver_methodに対して引数を渡せる仕組みです
- graphql_nameは、fieldの名前を取得できるようです
https://graphql-ruby.org/fields/introduction.html#extra-field-metadata
肝心のresolverメソッドであるload_assocはBaseObjectのなかに実装します
module ObjectTypes
class BaseObject < GraphQL::Schema::Object
field_class Loaders::LoadableField
def load_assoc(graphql_name:)
Loaders::AssociationLoader.for(object.class, graphql_name.underscore).load(object)
end
end
end
- graphql_nameはfieldの名前がキャメルケースで取得できるものなので、スネークケースに変換しています
これで完成!
意外とあっさり実装できました。
(実はこの実装にたどり着くまで紆余曲折有りました…)
後は、loaderを使って読み込みたいfieldにload: true
を追加していくだけです。
まとめ
- graphql-batchのloaderは、ボイラープレートの量が少し気になります
- graphql-rubyのfield拡張とextrasを使って、fieldのオプションだけでloaderを使うことができるようになる方法を紹介しました
- コード量を1/9に減らすことができたので、スッキリしました 🎉
Discussion