Graphql 不思議機能 dataloader の大きな2つの流れ
おはようございます!スペースマーケットのバックエンドエンジニアの小見と申します。
今回はGraphqlでN+1を解決するためのスタンダードな機能dataloader
これの動きが最初全然分からなかったですし、
自分以外のエンジニアも苦戦していました。
なので、今回極力どういった動きをするのかといったイメージをお伝えできればと思います。
dataloaderサンプルが以下になります。
概要
dataloader は Graphql のN+1 問題を解決する仕組みで、
多少名前が違うかも知れませんが、言語関係なく似たような機能のライブラリが存在します。
今回はgo なので以下ライブラリが対象です。
dataloader は大きく分けて2つの流れをたどります。
1.メソッドの呼び出し時のKey(引数) を溜め込む。
2.HttpRequestごとに溜め込んだKeyを元に一括実行を行います。
※厳密にはRequestに対して1回でなくなる場合もあります。
※主にdataloaderで実行するのはSQLやAPI問い合わせなどです。
1.メソッドの呼び出し時のKey(引数) を溜め込む
User エンティティのTodos を解決するためのResolverを定義しています。
以下のようにdataloader のLoadメソッドに引数を渡し引数を溜め込みます。
func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
.........
return For(ctx).TodosByUserIDs.Load(intID)
}
実際のコーディングを行っていると疑問になるのが、
Loadメソッドの戻り値が何なのかといったところですが
これはライブラリによりますが今回利用しているライブラリでは、
SQLが問い合わせた結果の情報が手に入りました。
複数のUserのTodosを解決する時には以下のようなイメージでkeysにUserIDを溜め込みます。
2.HttpRequestごとに溜め込んだKeyを元に一括実行
Loadメソッドがあちこちで呼ばれてKeyが集まった後にSQLで問い合わせを行います。
(コード上はidsが本文上のKeyのまとまりです。)
fetch: func(ids []int) ([][]*model.Todo, []error) {
var todos []*gormModel.Todo
db.Where("user_id IN ?", ids).Find(&todos)
todoByUserID := map[int][]*model.Todo{}
for _, todo := range todos {
todoByUserID[todo.UserID] = append(todoByUserID[todo.UserID], &model.Todo{
ID: strconv.Itoa(todo.ID),
Body: todo.Body,
UserID: strconv.Itoa(todo.UserID),
})
}
results := make([][]*model.Todo, len(ids))
for i, id := range ids {
results[i] = todoByUserID[id]
}
return results, nil
},
これまでの流れのイメージです。
この問い合わせた結果を呼び出し元に返して
完了になります。
まとめ
概念イメージは以上になるのですが、dataloaderがあれこれ
やってくれてしまっているのでイメージするのが難しいのだと思います。
自分が思った疑問が
そりゃIN句で一括でSQL実行できればいいだろうけど、
・HttpRequestのまとまりをどうやって判断するんだろう?
とか
・どういう実行順序で実行されるのだろうか?
とか
色々イメージが出来ない部分があり開発の手が止まった時がありました。
ただ、dataloader すべて把握することは一旦諦め以下の挙動をざっくりイメージすることである程度モヤモヤが晴れて開発をすすめることが出来ました。
* Keyを溜め込む
* 溜め込まれたKeyで処理を一括実行できる。
dataloader をこれから触るけどさっぱりイメージが出来ないと言った方の手助けになれば幸いです。
最後に弊社宣伝です。
弊社ではエンジニアを募集しております!
カジュアル面談も実施しておりますのでよろしければ以下よりご応募ください!