GoのGraphQLで使うdataloaderライブラリ「graph-gophers/dataloader」のv7が神だった
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を使いましょう!
めちゃくちゃ快適に実装できるのでぜひ参考にしてください!
以下にも参考になりそうな記事を貼っておきます。
Discussion