DataLoaderを用いてN+1問題対処(1対多のデータの取得)
間違っている部分、エラーが発生する部分、わかりにくい部分などがございましたら、是非ご指摘をお願い致します。
今回はPart5-2になります。
前回の内容を実装した上での実装になります。
前回:1対1のデータの取得の際のDataLoaderの実装を行いました。
(todoに紐づいている一人のユーザをDataLoaderを用いて取得しました)
この記事の内容
- 1対多のDataLoaderの実装(ユーザに対して紐づいている複数のTodoをDataLoaderを用いて取得する)
Github Repo
この記事で紹介するコードはGithubに置いています。
Part5-2までの切り取り
DataLoader(1対多)の実装
loader/todo.go
ファイルを作成します。
DataLoaderのtodoの取得処理を実装しています。
基本的には、loader/user.goと同じです。
package loader
import (
"context"
"fmt"
"github.com/graph-gophers/dataloader"
"github.com/shion0625/gqlgen-todos/graph/model"
"gorm.io/gorm"
"log"
)
// TodoLoader はデータベースからtodoを読み取ります
type TodoLoader struct {
DB *gorm.DB
}
// BatchGetTodos は、ID によって多くのtodoを取得できるバッチ関数を実装します。
func (u *TodoLoader) BatchGetTodos(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
// 単一のクエリで要求されたすべてのtodoを読み取ります
userIDs := make([]string, len(keys))
for ix, key := range keys {
userIDs[ix] = key.String()
}
todosTemp := []*model.Todo{}
if err := u.DB.Debug().Where("user_id IN ?", userIDs).Find(&todosTemp).Error; err != nil {
err := fmt.Errorf("fail get todos, %w", err)
log.Printf("%v\n", err)
return nil
}
todoByUserId := map[string][]*model.Todo{}
for _, todo := range todosTemp {
todoByUserId[todo.UserId] = append(todoByUserId[todo.UserId], todo)
}
todos := make([][]*model.Todo, len(userIDs))
for i, id := range userIDs {
todos[i] = todoByUserId[id]
}
output := make([]*dataloader.Result, len(todos))
for index := range todos {
todo := todos[index]
output[index] = &dataloader.Result{Data: todo, Error: nil}
}
return output
}
// dataloader.Loadをwrapして型づけした実装
func LoadTodo(ctx context.Context, todoID string) ([]*model.Todo, error) {
loaders := GetLoaders(ctx)
thunk := loaders.TodoLoader.Load(ctx, dataloader.StringKey(todoID))
result, err := thunk()
if err != nil {
return nil, err
}
return result.([]*model.Todo), nil
}
BatchGetTodos関数の説明
todosTemp := []*model.Todo{}
if err := u.DB.Debug().Where("user_id IN ?", userIDs).Find(&todosTemp).Error; err != nil {
err := fmt.Errorf("fail get todos, %w", err)
log.Printf("%v\n", err)
return nil
}
今回取得するkeyにあたる値はユーザIDと設定する予定なのでtodoテーブルからユーザIDを指定してデータの取得を行います。
todoByUserId := map[string][]*model.Todo{}
for _, todo := range todosTemp {
todoByUserId[todo.UserId] = append(todoByUserId[todo.UserId], todo)
}
map配列として今後扱っていくのでユーザIDをkeyとして設定しています。valueは同じuserIDを持つtodoの配列です。
このようなmapがあった場合
map{"U01": [], "U02": []}
下記のようなtodoが来ると
todo1{user_id:"U01", text:"user1"}
todo2{user_id:"U01", text:"user2"}
todo3{user_id:"U02", text:"user3"}
このようなmap配列になります
map{"U01": [todo1,todo2], "U02": [todo3]}
todos := make([][]*model.Todo, len(userIDs))
for i, id := range userIDs {
todos[i] = todoByUserId[id]
}
2重配列を作成します。そしてkeysとして入力された順番にtodos配列に配列データを保管していきます。
残りの部分に関してはuser.goと同じでtodoに書き替えただけです。
LoadTodoに関しては戻り値を[]*model.Todo
に書き換えれば大丈夫です!
loader/loaders.go
にtodoのloaderを追加します。
package loader
import (
"context"
"github.com/graph-gophers/dataloader"
"gorm.io/gorm"
"net/http"
)
type ctxKey string
const (
loadersKey = ctxKey("dataloaders")
)
// 各DataLoaderを取りまとめるstruct
type Loaders struct {
UserLoader *dataloader.Loader
+ TodoLoader *dataloader.Loader
}
// Loadersの初期化メソッド
func NewLoaders(db *gorm.DB) *Loaders {
//ローダーの定義
userLoader := &UserLoader{
DB: db,
}
+ todoLoader := &TodoLoader{
+ DB: db,
+ }
loaders := &Loaders{
UserLoader: dataloader.NewBatchedLoader(userLoader.BatchGetUsers),
+ TodoLoader: dataloader.NewBatchedLoader(todoLoader.BatchGetTodos),
}
return loaders
}
...
そして最後にtodosにloaderを使用する処理を記述すれば完成です。
func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
- todo := []*model.Todo{}
- r.DB.Debug().Where("user_id = ?", obj.ID).Find(&todo)
+ todo, err := loader.LoadTodo(ctx, obj.ID)
+ if err != nil {
+ return nil, err
+ }
return todo, nil
}
まとめ
DataLoaderを用いたN+1問題の対処(1対多の場合)を行いました。
今回の実装に関してはそこまで難易度は高くないかなと思います。
次回はrepositoryにデータベースを叩く処理を分離します(超簡単-しなくても問題なし)。また、バリデーションの設定を行っていきます。