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

# DataLoaderを用いてN+1問題対処(1対多のデータの取得）

:::note warn
間違っている部分、エラーが発生する部分、わかりにくい部分などがございましたら、是非ご指摘をお願い致します。
:::

## 今回はPart5-2になります。

**前回の内容を実装した上での実装になります。**

前回：1対１のデータの取得の際のDataLoaderの実装を行いました。
(todoに紐づいている一人のユーザをDataLoaderを用いて取得しました）

https://qiita.com/shion0625/items/47c2b73c1b2d652d8e6c

## この記事の内容
- 1対多のDataLoaderの実装（ユーザに対して紐づいている複数のTodoをDataLoaderを用いて取得する）

## Github Repo
この記事で紹介するコードはGithubに置いています。

**Part5-2までの切り取り**

https://github.com/shion0625/gqlgen-todos/tree/part5-2

## DataLoader(1対多)の実装

`loader/todo.go`ファイルを作成します。
DataLoaderのtodoの取得処理を実装しています。
基本的には、loader/user.goと同じです。

```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
}
```

<details>
<summary>BatchGetTodos関数の説明</summary>

```go: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を指定してデータの取得を行います。

```go:BatchGetTodos関数
	todoByUserId := map[string][]*model.Todo{}
	for _, todo := range todosTemp {
		todoByUserId[todo.UserId] = append(todoByUserId[todo.UserId], todo)
	}
```
map配列として今後扱っていくのでユーザIDをkeyとして設定しています。valueは同じuserIDを持つtodoの配列です。
```go
このような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]}
```

```go:BatchGetTodos関数
	todos := make([][]*model.Todo, len(userIDs))

	for i, id := range userIDs {
		todos[i] = todoByUserId[id]
	}
```
2重配列を作成します。そしてkeysとして入力された順番にtodos配列に配列データを保管していきます。

残りの部分に関してはuser.goと同じでtodoに書き替えただけです。
</details>

**LoadTodo**に関しては戻り値を`[]*model.Todo`に書き換えれば大丈夫です！


`loader/loaders.go`にtodoのloaderを追加します。
```diff_go: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を使用する処理を記述すれば完成です。

```diff_go: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問題の対処（１対多の場合）を行いました。
今回の実装に関してはそこまで難易度は高くないかなと思います。

次回はrepositoryにデータベースを叩く処理を分離します(超簡単-しなくても問題なし)。また、バリデーションの設定を行っていきます。