💿

GoのGraphQLで使うdataloaderライブラリ「graph-gophers/dataloader」のv7が神だった

2023/07/30に公開

GraphQLで使うdataloaderが難しすぎる・・・!と思っていた

業務などでGraphQLのAPIサーバを書いているときに、(dataloaderってめちゃくちゃ難しいな・・・)と思ってしぶしぶ取り組んでいました。
ただ、ライブラリをそれまで使っていたdataloadenからgraph-gophers/dataloaderのv7に変えたとき、世界が変わりました。なので今回はgraph-gophers/dataloaderのv7をどのように使ったかについて書いていきたいと思います。

この記事で触れること

  • graph-gophers/dataloader/v7をどう活用したか

この記事で触れないこと

  • dataloaderとは何か?
  • なぜdataloaderが必要か?
    上記のような話については、こちらのLayerXさんの記事などを参考にしてみてください。

既存のgraph-gophers/dataloaderの特徴

既存のdataloaderは以下のような流れで実装します。

  • usecaseに対してbatchFuncを定義
    • batchFuncにrepositoryなどの依存性を直接注入するように作る
  • Echoのmiddlewareでcontextに追加する際にインスタンス生成
type sampleKey struct{}
var sampleKey  sampleKey = struct{}
// 中略
ctx = context.WithValue(
	ctx, 
	sampleKey, 
	dataloader.NewBatchedLoader(
		mypackage.NewBatchFunc(repository).BatchFunc,
	),
)

// batchFunc
package mypackage
type (
	BatchFunc interface {
		BatchFunc() func(context.Context, []int64) dataloader.Result
	}
	batchFuncImpl struct {
		// repository層を注入
		repository myrepository.Repository
	}
)
func NewBatchFunc(r myrepository.Repository) BatchFunc{
	return &batchFuncImpl{
		repository: r,
	},
}

func (b *batchFuncImpl)BatchFunc()func(context.Context, []int64) dataloader.Result{
	return func(ctx context.Context, keys []int64)dataloader.Result{
		// batchfunc...
	}
}

これはこれでわかりやすいのですが、usecaseの数だけmypackage.NewBatchFuncのkeyの型とResultの型を意識して書いて、呼び出し元で型をアサーションしないといけませんでした。

graph-gophers/dataloader/v7の特徴

一言で言うと、graph-gophers/dataloaderのV7はジェネリクスを用いていることが特徴です。

type Interface[K comparable, V any] interface {
	Load(context.Context, K) Thunk[V]
	LoadMany(context.Context, []K) ThunkMany[V]
	Clear(context.Context, K) Interface[K, V]
	ClearAll() Interface[K, V]
	Prime(ctx context.Context, key K, value V) Interface[K, V]
}

dataloader.Interfaceに型を与えて実装してしまえばいいだけなので、(このバッチ関数はkey何にしたっけ・・・?)みたいなことが減るのと、あとは独自にLoader.Loadを呼び出すだけの関数を持ったinterfaceなどを作らなくていいのも地味にメリットかなと思っています。

Loadする際に読み込むバッチ関数はどこで定義するのかというと、これもすでに用意があります。

func NewBatchedLoader[K comparable, V any](batchFn BatchFunc[K, V], opts ...Option[K, V]) *Loader[K, V]

*Loader[K, V]がInterface[K,V]を実装しているので、とくにこの辺りは何も考えず実装できます。

リポジトリ層などのDI(依存性注入)を行うため独自のinterfaceを定義する

あとは上記の仕組みを利用したinterfaceを独自に定義します。この独自interfaceを実装するstructにリポジトリ層の実装を持たせておくことで、依存性を注入して利用することができます。

import "github.com/graph-gophers/dataloader/v7"
// interfaceを実装
type (
	Dataloader[K comparable, V any] interface {
		// dataloader.Interface[K,V]を返す関数
		NewLoaderInterface() dataloader.Inerface[K,V]
		// NewLoaderInterfaceの内部で使用する関数。privateで実装していたがテストの時などにパッケージが狂って呼び出せなくなるのでpublicにした
		NewBatchFunc() dataloader.BatchFunc[C, V]
	}
	
	// 型パラメータに与える型を明示したtypeエイリアスを作ってこっちを使う
	MyDataloader Dataloader[int64, mymodels.Model]
	dataloaderImpl struct {
		repo: myrepository.Repository
	}
	
)

func NewMyDataloader(r myrepository.Repository) MyDataloader {
	return &dataloaderImpl{
		repo: r,
	},
}

func(d *dataloaderImpl) NewLoaderInterface() dataloader.Interface[int64, mymodels.Model]{
	// 第二引数にoptionも入れられるが、あんまり恩恵をまだ感じ取れていないため割愛
	return NewBatchedLoader[int64, mymodels.Model](d.NewBatchFunc)
}
func(d *dataloaderImpl) NewBatchFunc() dataloader.BatchFunc[int64, mymodels.Model] {
	// please implement your batch func.
}

あとは、NewMyDataloaderをmiddlewareで呼び出してcontextに含めておくことで、リクエスト単位でN+1問題を解決可能です。

Go+GraphQLでdataloader実装に困っている人は直ちにgraph-gophers/dataloader/v7を使いましょう!

めちゃくちゃ快適に実装できるのでぜひ参考にしてください!
以下にも参考になりそうな記事を貼っておきます。

参考

N+1問題の回避 - dataloaderの導入(素晴らしい記事)

Discussion