LoginSignup
2
0

More than 1 year has passed since last update.

gqlgenでDataLoaderを用いてN+1 問題対処 (1対多のデータの取得)【実装編-Part4-2】

Last updated at Posted at 2023-01-04

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と同じです。

loader/todo.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関数の説明
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を指定してデータの取得を行います。

BatchGetTodos関数
	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]}
BatchGetTodos関数
	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を追加します。

loader/loaders.go
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を使用する処理を記述すれば完成です。

graph/schema.resolvers.go
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にデータベースを叩く処理を分離します(超簡単-しなくても問題なし)。また、バリデーションの設定を行っていきます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0