GraphQLとかN+1とかその辺りの話 with Go

前回の記事（[GoでGraphQLを話すサーバを作ってみた](https://qiita.com/ichikawa_0829/items/964682e3450d828968b9)）をDBから取得するようにしてみたが、
どうにも気になる事象（タイトルの通りだがN+1問題）が発生したので更にメモ。

# テーブル

users

| Field | Type | Null| Key |
|:------|:-----|:----|:----|
| id | varchar(255) | NO | PRI |	
|name| varchar(255) | NO ||

todo

| Field | Type | Null| Key |
|:------|:-----|:----|:----|
|id|varchar(255)|NO|PRI|		
|user_id|varchar(255)|NO||			
|text|varchar(255)|NO||		

# N+1問題が発生していることを確認

```
mysql> select * from users;
+---------------------+------------+
| id                  | name       |
+---------------------+------------+
| 8674665223082153551 | s-ichikawa |
+---------------------+------------+
1 row in set (0.00 sec)

mysql> select * from todos;
+----------------------+---------------------+-------+
| id                   | user_id             | text  |
+----------------------+---------------------+-------+
| T3916589616287113937 | 8674665223082153551 | Todo3 |
| T4037200794235010051 | 8674665223082153551 | Todo2 |
| T6129484611666145821 | 8674665223082153551 | Todo1 |
+----------------------+---------------------+-------+
3 rows in set (0.00 sec)
```

とレコードが登録されていたとして、
こんなGraphQLを叩く
<img width="600" alt="スクリーンショット 2018-07-01 15.11.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/f3cb23e5-d407-aca5-d243-42f478d79da6.png">

すると以下のようにtodos全件を取得するクエリが1件とtodosに紐づくusersレコードを取得するクエリがtodosのレコード数分呼ばれる。

```:/var/log/mysql/general.log
10 Query	SELECT id, user_id, text FROM todos
12 Prepare	SELECT id, name FROM users  WHERE id = ?
10 Prepare	SELECT id, name FROM users  WHERE id = ?
12 Execute	SELECT id, name FROM users  WHERE id = '8674665223082153551'
10 Execute	SELECT id, name FROM users  WHERE id = '8674665223082153551'
12 Close stmt
10 Close stmt
13 Connect	root@172.17.0.1 on gql_todo using TCP/IP
13 Prepare	SELECT id, name FROM users  WHERE id = ?
13 Execute	SELECT id, name FROM users  WHERE id = '8674665223082153551'
13 Close stmt
13 Quit
```

つまり典型的なN+1問題が発生していて、このまま本番環境で使うものとして採用すると場合によっては辛い未来が待っている事が予想される。

## Deta Loaderを使ってみる

[vektah/dataloaden]()という[facebook/dataloader](https://github.com/facebook/dataloader)にインスパイアされて作成されたというGraphQLのためのData Loader Generatorがあります。
これで作ったData Loaderでtodosに紐づくuserを取得する際に1件づつSQLを発行するのではなく、最終的にデータを返す際にまとめてSQLを発行できるようにします。

まずはインストール

```
go get -u github.com/vektah/dataloaden
```

次にGenerateコマンドを実行します。
今回はusersを取得するためのキーがstringであるため`-keys="string"`オプションをつけています。

```
$ dataloaden -keys="string" github.com/s-ichikawa/gql-todo/graph.User
```

するとgraph.Userの定義に従ってuserloader_gen.goというファイルが作成されます。
これを使って、http.Handlerにかませるミドルウェアを書いていきます。

```
const userLoaderKey = "userloader"

func DataloaderMiddleware(db *model.DBModel, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		userloader := UserLoader{
			maxBatch: 100,
			wait:     1 * time.Millisecond,
			fetch: func(ids []string) ([]*User, []error) {
				placeholders := make([]string, len(ids))
				args := make([]interface{}, len(ids))
				for i := 0; i < len(ids); i++ {
					placeholders[i] = "?"
					args[i] = i
				}

				rows, err := db.GetUsers(model.SearchUserCondition{
					Ids: ids,
				})
				if err != nil {
					panic(err)
				}

				var users []*User
				for rows.Next() {
					var user User
					rows.Scan(&user.ID, &user.Name)

					users = append(users, &user)
				}

				return users, nil
			},
		}
		ctx := context.WithValue(r.Context(), userLoaderKey, &userloader)
		r = r.WithContext(ctx)
		next.ServeHTTP(w, r)
	})
}
```

`UserLoader`はdataloadenコマンドによって生成されたコードに含まれる構造体です。
macBachは1度に検索する最大数上の例だと100件毎にクエリが発行される。
waitは検索する間隔の指定
fetchはデータを解決するための関数
をそれぞれ指定・実装します。（他にもあったけど後で調べる）
これをcontextに持たせてResolver部分を改修していきます。

todosに紐づくusersを解決するためのResolver部分は現在以下のようになっているとして、

```
func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (*User, error) {
	// DBからusersを取得する
	users, err := r.getUsers(model.SearchUserCondition{
		Ids: []string{obj.UserId},
	})
	if err != nil {
		return &User{}, fmt.Errorf("Select error: %s", err)
	}

	return &users[0], nil
}
```

以下のようにcontextからData Loaderを呼び出して、そこからLoadすようにする。

```
func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (*User, error) {
	return ctx.Value(userLoaderKey).(*UserLoader).Load(obj.UserId)
}
```

こうする事で全レコード数からmaxBench数を解決する毎にUserLoader.fetchが呼ばれその結果をキャッシュ。
以降必要なUserIdのデータをキャッシュから取得することが可能になる。

ということで試してみる
<img width="600" alt="スクリーンショット 2018-07-01 15.11.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/f3cb23e5-d407-aca5-d243-42f478d79da6.png">

実際にクエリが発行される回数が減ったか確認してみる。

```:/var/log/mysql/general.log
50 Connect	root@172.17.0.1 on gql_todo using TCP/IP
50 Query	SELECT id, user_id, text FROM todos
50 Prepare	SELECT id, name FROM users  WHERE id IN (?)
50 Execute	SELECT id, name FROM users  WHERE id IN ('8674665223082153551')
50 Close stmt
```

やったぜ。

一応コードはこちら
https://github.com/s-ichikawa/gql-todo