go+gqlgenでGraphQLサーバを作る（GORM使ってDB接続）

# お題
表題の通り。GraphQL関連では[前々回](https://qiita.com/drafts/8abd445edba347e8f6f1)、frontend に「nuxtjs/apollo」、backend に「go+gqlgen」の組み合わせでGraphQLサービスを作るというのをやった。
その時は backend は単に固定のJSONを返すだけでお茶を濁したのだけど、もちろんサービスとして成り立たせる上で固定のJSON返すだけなんてことはない。
ので、今回は、永続化実装。

# 関連記事索引
- 第12回「[GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)](https://qiita.com/sky0621/items/957c010ba01c7a756ef7)」
- 第11回「[Dataloadersを使ったN+1問題への対応](https://qiita.com/sky0621/items/621f075e4257270a9e02)」
- 第10回「[GraphQL(gqlgen)エラーハンドリング](https://qiita.com/sky0621/items/affc833e770fee8b576e)」
- 第９回「[GraphQLにおける認証認可事例（Auth0 RBAC仕立て）](https://qiita.com/sky0621/items/efbb8fa085094230d0fd)」
- 第８回「[GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例](https://qiita.com/sky0621/items/5c2c565678c102d9d4ed)」
- 第７回「[GraphQLにおけるRelayスタイルによるページング実装（後編：フロントエンド）](https://qiita.com/sky0621/items/fdcdaefeef7cf4b750ac)」
- 第６回「[GraphQLにおけるRelayスタイルによるページング実装（前編：バックエンド）](https://qiita.com/sky0621/items/1e8823200633f2c46013)」
- 第５回「[DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動](https://qiita.com/sky0621/items/58fa2bcfc0935f6186cb)」
- 第４回「[graphql-codegenでフロントエンドをGraphQLスキーマファースト](https://qiita.com/sky0621/items/f03631f0309f5c375b1d)」
- 第３回「[go+gqlgenでGraphQLサーバを作る（GORM使ってDB接続）](https://qiita.com/sky0621/items/32e3e26189589080e492)」
- 第２回「[NuxtJS(with Apollo)のTypeScript対応](https://qiita.com/sky0621/items/0297f5a026be380a5929)」
- 第１回「[frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る](https://qiita.com/sky0621/items/8abd445edba347e8f6f1)」

# 開発環境
## # OS - Linux(Ubuntu)
```
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

## # バックエンド
### 言語 - Go
```
$ go version
go version go1.13.3 linux/amd64
```

### パッケージマネージャ - Go Modules

### IDE - Goland
```
GoLand 2019.2.5
Build #GO-192.7142.48, built on November 8, 2019
```

## # Dockerコンテナ
### Docker
```
$ docker -v
Docker version 18.09.2, build 6247962
```

### docker-compose
```
$ docker-compose -v
docker-compose version 1.23.1, build b02f1306
```

## # DB接続ツール
### DataGrip
```
DataGrip 2019.3.1
Build #DB-193.5662.58, built on December 18, 2019
```

# 参考
## GraphQL
https://graphql.org/
https://gqlgen.com/

## O-Rマッパー
https://gorm.io/ja_JP/

# 実践
今回の全ソースは下記。
https://github.com/sky0621/study-graphql/tree/v0.3.0

## 設計
TODOアプリの真似事。
以下の機能を持つ。

- ユーザ情報を１件登録
- ユーザ情報を１件取得（★そのユーザと紐づくTODO情報も同時に取得可能）
- 全ユーザ情報を取得（★同上）
- TODO情報を１件登録
- TODO情報を１件取得（★そのTODOを登録したユーザ情報も同時に取得）
- 全TODO情報を取得（★同上）

## ローカルにDBサーバ立てる
[docker-compose](http://docs.docker.jp/compose/toc.html)使うのでYamlを書く。
DBは何でもよかったので、とりあえずMySQL。

```yaml:[docker-compose.yml]
version: '3'
services:
  db:
    restart: always
    image: mysql:5.7.24
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      MYSQL_DATABASE: localdb
    volumes:
      - ./persistence/init:/docker-entrypoint-initdb.d
```

あと、Docker起動時にコンテナ内のデータベースにテーブル作成したいので以下のDDLも用意。

```sql:[persistence/init/1_create.sql]
CREATE TABLE IF NOT EXISTS `todo` (
  `id` varchar(64) NOT NULL,
  `text` varchar(256) NOT NULL,
  `done` bool NOT NULL,
  `user_id` varchar(64) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;

CREATE TABLE IF NOT EXISTS `user` (
  `id` varchar(64) NOT NULL,
  `name` varchar(256) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
```

準備できたので起動。

```
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$
$ ll docker-compose.yml 
-rw-r--r-- 1 sky0621 sky0621 399 Dec 22 20:12 docker-compose.yml
$
$ ls -lR persistence/
persistence/:
total 8
drwxr-xr-x 2 sky0621 sky0621 4096 Dec 22 20:14 init
-rw-r--r-- 1 sky0621 sky0621   99 Dec 22 19:18 README.md

persistence/init:
total 4
-rw-r--r-- 1 sky0621 sky0621 405 Dec 22 20:14 1_create.sql
$
$ sudo docker-compose up
Creating network "study-graphql_default" with the default driver
Creating study-graphql_db_1_3a13f8e517b8 ... done
Attaching to study-graphql_db_1_f411e51b48e6
db_1_f411e51b48e6 | Initializing database
　　・
　　・
　　・
db_1_f411e51b48e6 | 2019-12-27T17:02:30.129056Z 0 [Note] mysqld: ready for connections.
db_1_f411e51b48e6 | Version: '5.7.24'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
```

テーブル作成もできているか確認。
![Screenshot from 2019-12-28 02-05-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/2a13898d-57e8-8c78-df1d-8f91d8841ee7.png)
うん、OK。

## GraphQLスキーマ

最初に「TODO情報」と「ユーザ情報」のモデルを定義。
「TODO情報」モデルの中には「ユーザ情報」モデルを含んでおり、「ユーザ情報」モデルの中にも”複数の”「TODO情報」モデルを含んでいる。
このように定義しておくと、クライアントからこのモデルを取得するクエリを発行する際に階層構造ごと１クエリで取得することができる。

次に以下４つのクエリを定義。
・全TODO情報取得
・１TODO情報取得
・全ユーザ情報取得
・１ユーザ情報取得

次は新規作成用のパラメータを定義。
・「`NewTodo`」という名でTODO登録時のパラメータを定義
・「`NewUser`」という名でユーザ登録時のパラメータを定義

最後に以下２つのミューテーションを定義。
・TODO情報登録
・ユーザ情報登録

```graphql:[schema/schema.graphql]
# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
  todos: [Todo!]!
}

type Query {
  todos: [Todo!]!
  todo(id: ID!): Todo!

  users: [User!]!
  user(id: ID!): User!
}

input NewTodo {
  text: String!
  userId: String!
}

input NewUser {
  name: String!
}

type Mutation {
  createTodo(input: NewTodo!): ID!
  createUser(input: NewUser!): ID!
}
```

## Go実装
### main関数にサーバ起動時のDBコネクション初期化処理を追加

```go:[server/server.go]
package main

import (
	"log"
	"net/http"
	"os"

	"github.com/99designs/gqlgen/handler"
	"github.com/jinzhu/gorm"
	"github.com/sky0621/study-graphql/backend"

	_ "github.com/go-sql-driver/mysql"
)

const dataSource = "localuser:localpass@tcp(127.0.0.1:3306)/localdb?charset=utf8&parseTime=True&loc=Local"
const defaultPort = "5050"

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	// 主にここの処理
	db, err := gorm.Open("mysql", dataSource)
	if err != nil {
		panic(err)
	}
	if db == nil {
		panic(err)
	}
	defer func() {
		if db != nil {
			if err := db.Close(); err != nil {
				panic(err)
			}
		}
	}()
	db.LogMode(true)

	http.Handle("/", handler.Playground("GraphQL playground", "/query"))
	http.Handle("/query", handler.GraphQL(backend.NewExecutableSchema(backend.Config{Resolvers: &backend.Resolver{DB: db}})))

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}
```

### モデル定義
続いて、GraphQLスキーマにも定義した「TODO情報」モデルと「ユーザ情報」モデルのGo実装。

```go:[models/models.go]
package models

type Todo struct {
	ID   string `json:"id"`
	Text string `json:"text"`
	Done bool   `json:"done"`
}

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}
```

GraphQLスキーマで定義したモデルと比較するとわかるのだけど、例えば「TODO情報」モデル。
GraphQLスキーマでは、こう。

```graphql
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}
```

Go側の実装には「`user: User!`」に該当する要素がない。
「ユーザ情報」モデルもしかり。

```graphql
type User {
  id: ID!
  name: String!
  todos: [Todo!]!
}
```

「TODO情報」モデルの配列がGo側の実装にはない。

これが実はポイント。

### gqlgen関連
上記で”ポイント”としたのは、以下のように gqlgen を使った各種ソース自動生成をするとわかる。

```yaml:[gqlgen.yml]
schema:
- ../schema/schema.graphql
exec:
  filename: generated.go
model:
  filename: models_gen.go
resolver:
  filename: resolver.go
  type: Resolver
autobind:
  - github.com/sky0621/study-graphql/backend/models

models:
  User:
    model: models.User
  Todo:
    model: models.Todo
```

ソース自動生成

```
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/backend
$
$ ll gqlgen.yml 
-rw-r--r-- 1 sky0621 sky0621 388 Dec 22 23:32 gqlgen.yml
$
$ go run github.com/99designs/gqlgen init
　　・
　　・
　　・
$
$ ll resolver.go 
-rw-rw-r-- 1 sky0621 sky0621 3.7K Dec 26 01:07 resolver.go
```

gqlgenコマンドの使用については以下参照。
https://qiita.com/sky0621/items/8abd445edba347e8f6f1#gqlgenコマンドによるスケルトン生成

自動生成された`resolver.go`の中身は。

```go:[resolver.go]
package backend

import (
	"context"

	"github.com/sky0621/study-graphql/backend/models"
)

// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.

type Resolver struct{}

func (r *Resolver) Mutation() MutationResolver {
	return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
	return &queryResolver{r}
}
func (r *Resolver) Todo() TodoResolver {
	return &todoResolver{r}
}
func (r *Resolver) User() UserResolver {
	return &userResolver{r}
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (string, error) {
	panic("not implemented")
}
func (r *mutationResolver) CreateUser(ctx context.Context, input NewUser) (string, error) {
	panic("not implemented")
}

type queryResolver struct{ *Resolver }

func (r *queryResolver) Todos(ctx context.Context) ([]*models.Todo, error) {
	panic("not implemented")
}
func (r *queryResolver) Todo(ctx context.Context, id string) (*models.Todo, error) {
	panic("not implemented")
}
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
	panic("not implemented")
}
func (r *queryResolver) User(ctx context.Context, id string) (*models.User, error) {
	panic("not implemented")
}

type todoResolver struct{ *Resolver }

func (r *todoResolver) User(ctx context.Context, obj *models.Todo) (*models.User, error) {
	panic("not implemented")
}

type userResolver struct{ *Resolver }

func (r *userResolver) Todos(ctx context.Context, obj *models.User) ([]*models.Todo, error) {
	panic("not implemented")
}
```

まあ、いろいろ書いてあるのだけど、これらは全てGraphQLスキーマに定義したクエリとミューテーションを実行する上で必要なロジック群。
この中でも、先ほど言った「GraphQLスキーマにあった『`user: User!`』がGoのモデルにはない」が関係してくるのが以下。
・`todoResolver`の`User`メソッド
・`userResolver`の`Todos`メソッド

どう関係してくるかについては、実装して動作確認するのが一番わかりやすいので、まずは実装。

### ユーザ登録（TODO登録は省略）

クライアントから以下の要求があると、

```graphql:[schema/schema.graphql]
type Mutation {
  createUser(input: NewUser!): ID!
}

input NewUser {
  name: String!
}
```

リゾルバーの以下が発火し、

```go:[resolver.go]
func (r *mutationResolver) CreateUser(ctx context.Context, input NewUser) (string, error) {
	log.Printf("[mutationResolver.CreateUser] input: %#v", input)
	id := util.CreateUniqueID()
	err := database.NewUserDao(r.DB).InsertOne(&database.User{
		ID:   id,
		Name: input.Name,
	})
	if err != nil {
		return "", err
	}
	return id, nil
}
```

```go:[models_gen.go]
type NewUser struct {
	Name string `json:"name"`
}
```

user テーブルに対象ユーザ情報を登録する。

```go:[database/user.go]
func (d *userDao) InsertOne(u *User) error {
	res := d.db.Create(u)
	if err := res.Error; err != nil {
		return err
	}
	return nil
}

type User struct {
	ID   string `gorm:"column:id;primary_key"`
	Name string `gorm:"column:name"`
}

func (u *User) TableName() string {
	return "user"
}
```

実践してみると、以下の通り。
![Screenshot from 2019-12-28 03-18-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/a03e4ebf-3fad-7f4b-b567-d0f822db3b31.png)

データベース上も、反映されている。
![Screenshot from 2019-12-28 03-19-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/a21a2317-48da-a7fc-a1c8-4088f65e03cb.png)

### １ユーザ取得（TODO取得は省略）

クライアントから以下の要求があると、

```graphql:[schema/schema.graphql]
type Query {
  user(id: ID!): User!
}

type User {
  id: ID!
  name: String!
  todos: [Todo!]!
}

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}
```

リゾルバーの以下が発火し、

```go:[resolver.go]
func (r *queryResolver) User(ctx context.Context, id string) (*models.User, error) {
	log.Printf("[queryResolver.User] id: %s", id)
	user, err := database.NewUserDao(r.DB).FindOne(id)
	if err != nil {
		return nil, err
	}
	return &models.User{
		ID:   user.ID,
		Name: user.Name,
	}, nil
}
```

*↓重ね重ね言うけど、GraphQLスキーマのモデルにはある「複数のTODO情報」に関する要素がGoのモデルには無い*

```go:[models/models.go]
type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}
```

user テーブルから対象ユーザ情報を取得する。

```go:[database/user.go]
func (d *userDao) FindOne(id string) (*User, error) {
	var users []*User
	res := d.db.Where("id = ?", id).Find(&users)
	if err := res.Error; err != nil {
		return nil, err
	}
	if len(users) < 1 {
		return nil, nil
	}
	return users[0], nil
}
```

実践してみると、以下の通り。
![Screenshot from 2019-12-28 03-47-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/ae182948-8e45-b9ba-6343-3929d1aa0834.png)

ん？
Goのモデル上は、Userの中にTodo配列は持っていないのに、レスポンスに "todos" が含まれている・・・。

これ、実は、リゾルバーの`queryResolver.User`が発火して、その処理が終わった後、続いて以下も発火している。
以下の`Todos`の引数にある`obj *models.User`に、上記でDBから取得したユーザ情報が入っているので、その`obj`から`ID`を取得してユーザに紐づくTODO群を取得する処理が流せるというしかけ。

```go:[resolver.go]
func (r *userResolver) Todos(ctx context.Context, obj *models.User) ([]*models.Todo, error) {
	log.Println("[userResolver.Todos]")
	todos, err := database.NewTodoDao(r.DB).FindByUserID(obj.ID)
	if err != nil {
		return nil, err
	}
	var results []*models.Todo
	for _, todo := range todos {
		results = append(results, &models.Todo{
			ID:   todo.ID,
			Text: todo.Text,
			Done: todo.Done,
		})
	}
	return results, nil
}
```

# まとめ
今回のように実装すると、TODO情報取得クエリならTODO情報取得のリゾルバーが発火し、ユーザ情報取得クエリならユーザ情報取得のリゾルバーが発火する。
各々の処理を実装しておけば、GraphQLライブラリの中で適宜、必要な処理を呼び、結果をGraphQLスキーマのレスポンス構造に合わせてクライアントに返してくれる。
ここで問題が１つ。
ユーザ情報取得クエリでは、「TODO情報」モデルの中に「ユーザ情報」モデルが含まれていて、TODO情報を取得するたびに紐づくユーザ情報の取得も走る。
つまり、TODO情報がDBに100件あるとしたら、「全TODO情報」取得クエリをクライアントから発行し、レスポンスに含める要素として「ユーザ情報」も要求した場合、以下の通り101回のデータベースIOが発生する。
・全TODO情報を todo テーブルから select するSQL発行
・上記取得結果の１TODO情報ごとに、紐づくユーザ情報を取得するための user テーブルへの select するSQL発行

N+1問題というやつ。
これを解消するのは簡単で、全TODO情報を取得するSQLの中で user テーブルをJOINして持ってくれば101回のSQLなんてならず１SQLで済む。
ただ、そうすると、全TODO情報を取得するクエリをクライアントから実行する時、たとえ「今回はレスポンスにユーザ情報含めなくていい」ケースでも、サーバ側で必ず user テーブルをJOINして select するSQLが発行されることになる。
かといって、それを避けるには、レスポンスに何を求められているかに応じてSQL文を切り替えるなんてロジックを実装しなくちゃいけなくなる。
できるならどちらも避けたい。という問題への対処として、[dataloader](https://github.com/graphql/dataloader)というライブラリ(?)があるらしい。
次回以降のどこかで試してみようかな。以下など参考になりそう。
https://qiita.com/yuku_t/items/2c1735cbf45e75c0bfb8