Go + Nuxt + GraphQLで簡単カンバンアプリチュートリアル on docker-compose（サーバーサイド編）

## はじめに
* Go/Vue/GraphQL初学者が学習用にCRUD操作のできるアプリを作ってみた
* サーバーサイド編とフロントエンド編に分けてその手順を紹介
* ざっくりとした構成は
  * サーバー（Go + gqlgen）
  * フロント（Nuxt + Apollo）
  * データベース（MySQL）
  * ORM(gorm)
  * 開発環境（docker-compose）

## 完成イメージ
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/a07236a8-2412-38fa-ade6-f112fc8fa786.gif)
※ タスクの並び順は操作できません

## 関連リンク
* [ソースコード](https://github.com/MrFuku/kanban-go-nuxt-graphql)
* [フロントエンド編](https://qiita.com/iPzYN2gLSV2HeSS/items/d8d6bfebbbd0ef88a310)

## 免責事項
* GoもVueもそんなに知見がありません
* そのため見苦しい書きぶりをしている箇所が多々ありますがご容赦ください🙏

## データベース

### コンテナ情報の定義

```bash
# 適当なディレクトリにプロジェクトを作成します
❯ mkdir kanban-go-nuxt-graphql
❯ cd kanban-go-nuxt-graphql
```
`docker-compose.yml`を作成し、dbコンテナを定義します

```yaml:docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.24
    ports:
      - 3306:3306
    environment:
      TZ: 'Asia/Tokyo'
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      MYSQL_DATABASE: localdb
    volumes:
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
      - mysql_data:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
volumes:
  mysql_data:
```
コンテナを起動し、DBブラウザ等で下記SQLを実行しテーブルを作成する

```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;
```
こんな感じでテーブルが作成されればOK
![スクリーンショット 2020-05-31 13.50.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/373cdb60-8e11-d23b-ce2c-8093ec87b079.png)

## Go(gqlgen)
### ホットリロード開発環境

`server/Dockerfile`を作成し、`docker-compose.yml`にserverコンテナを追加します
※`server`ディレクトリも作成。以後、サーバー側のコードはこの配下に置きます

```dockerfile:server/Dockerfile
FROM golang:1.14-alpine

WORKDIR /go/src/github.com/MrFuku/kanban-go-nuxt-graphql/server

COPY . .

ENV GO111MODULE=on

RUN apk add git

RUN go get github.com/pilu/fresh

CMD ["fresh"]
```

```yaml:docker-compose.yml
version: '3'
services:
  # serverコンテナを追加
  server:
    build: ./server
    tty: true
    ports:
      - 8080:8080
    environment:
      LANG: ja_JP.UTF-8
      TZ: Asia/Tokyo
    volumes:
      - ./server/:/go/src/github.com/MrFuku/kanban-go-nuxt-graphql/server
  db:
    image: mysql:5.7.24
    ports:
      - 3306:3306
    environment:
      TZ: 'Asia/Tokyo'
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      MYSQL_DATABASE: localdb
    volumes:
      - mysql_data:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
volumes:
  mysql_data:
```
`go.mod`の作成

```bash
# serverコンテナを立ち上げ、shを起動
❯ docker-compose run server sh

# go.mod ファイルの作成
❯ go mod init
```
これでホットリロードの開発ができる環境が整いました

### gqlgelスキーマ定義

gqlgenの自動ファイル生成機能を使い、GraphQLスキーマの雛形を生成します

```bash
# serverコンテナを立ち上げ、shを起動
❯ docker-compose run server sh

# ファイルの自動生成
❯ go run github.com/99designs/gqlgen init
```
生成されたスキーマを修正し、今回のアプリに必要な機能を定義します

```graphql:server/graph/schema.graphqls
# GraphQL schema example
#
# https://gqlgen.com/getting-started/

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

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

type Query {
  todos: [Todo!]!
  users: [User!]!
  todo(id: String!): Todo!
  user(id: String!): User!
}

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

input EditTodo {
  id: String!
  text: String!
  done: Boolean!
  userId: String!
}

input NewUser {
  name: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
  updateTodo(input: EditTodo!): Todo!
  deleteTodo(input: String!): Todo!
  createUser(input: NewUser!): User!
}
```
`server/models/models.go`を作成し、modelの定義も併せて行います
※ ORMにgormを使用します

```go:server/models/models.go
package models

type Todo struct {
	ID     string `gorm:"column:id;primary_key"`
	Text   string `gorm:"column:text"`
	Done   bool   `gorm:"column:done"`
	UserID string `gorm:"column:user_id"`
}

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

func (u *Todo) TableName() string {
	return "todo"
}

func (u *User) TableName() string {
	return "user"
}
```
`server/gqlgen.yml`の設定を修正します

```yaml:server/gqlgen.yml
~ 省略 ~
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
  - "github.com/MrFuku/kanban-go-nuxt-graphql/server/models"

# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
  User:
    model: models.User
  Todo:
    model: models.Todo
```
定義した内容を元に、再度ファイルを自動生成


```bash
# serverコンテナに入った状態で実行
❯ go run github.com/99designs/gqlgen init
```

### リゾルバーの実装

スキーマ定義&自動ファイル生成を行ったことによりリゾルバーの雛形が作成されました
ここではリゾルバーを実装していきます

`server/util/util.go`の作成

```go:server/util/util.go
package util

import (
	"strings"

	"github.com/google/uuid"
)

func CreateUniqueID() string {
	return strings.Replace(uuid.New().String(), "-", "", -1)
}
```
`server/graph/resolver.go`にDBフィールド追加

```go:server/graph/resolver.go
package graph

import "github.com/jinzhu/gorm"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct {
	DB *gorm.DB
}
```
`server/graph/schema.resolvers.go`を修正

```go:server/graph/schema.resolvers.go
package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
	"context"

	"github.com/MrFuku/kanban-go-nuxt-graphql/server/graph/generated"
	"github.com/MrFuku/kanban-go-nuxt-graphql/server/graph/model"
	"github.com/MrFuku/kanban-go-nuxt-graphql/server/models"
	"github.com/MrFuku/kanban-go-nuxt-graphql/server/util"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*models.Todo, error) {
	todo := &models.Todo{
		ID:     util.CreateUniqueID(),
		Text:   input.Text,
		Done:   false,
		UserID: input.UserID,
	}
	res := r.DB.Create(todo)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todo, nil
}

func (r *mutationResolver) UpdateTodo(ctx context.Context, input model.EditTodo) (*models.Todo, error) {
	todo := &models.Todo{ID: input.ID}
	res := r.DB.First(todo)
	if err := res.Error; err != nil {
		return nil, err
	}

	params := map[string]interface{}{}
	params["text"] = input.Text
	params["done"] = input.Done
	params["user_id"] = input.UserID
	todo.Text = input.Text
	todo.Done = input.Done
	todo.UserID = input.UserID
	res = r.DB.Model(&todo).Update(params)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todo, nil
}

func (r *mutationResolver) DeleteTodo(ctx context.Context, input string) (*models.Todo, error) {
	todo := &models.Todo{ID: input}
	res := r.DB.First(todo)
	if err := res.Error; err != nil {
		return nil, err
	}
	res = r.DB.Delete(todo)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todo, nil
}

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*models.User, error) {
	user := &models.User{
		ID:   util.CreateUniqueID(),
		Name: input.Name,
	}
	res := r.DB.Create(user)
	if err := res.Error; err != nil {
		return nil, err
	}
	return user, nil
}

func (r *queryResolver) Todos(ctx context.Context) ([]*models.Todo, error) {
	var todos []*models.Todo
	res := r.DB.Find(&todos)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todos, nil
}

func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
	var users []*models.User
	res := r.DB.Find(&users)
	if err := res.Error; err != nil {
		return nil, err
	}
	return users, nil
}

func (r *queryResolver) Todo(ctx context.Context, id string) (*models.Todo, error) {
	todo := &models.Todo{ID: id}
	res := r.DB.First(&todo)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todo, nil
}

func (r *queryResolver) User(ctx context.Context, id string) (*models.User, error) {
	user := &models.User{ID: id}
	res := r.DB.First(user)
	if err := res.Error; err != nil {
		return nil, err
	}
	return user, nil
}

func (r *todoResolver) User(ctx context.Context, obj *models.Todo) (*models.User, error) {
	user := &models.User{ID: obj.UserID}
	res := r.DB.First(user)
	if err := res.Error; err != nil {
		return nil, err
	}
	return user, nil
}

func (r *userResolver) Todos(ctx context.Context, obj *models.User) ([]*models.Todo, error) {
	var todos []*models.Todo
	res := r.DB.Where("user_id = ?", obj.ID).Find(&todos)
	if err := res.Error; err != nil {
		return nil, err
	}
	return todos, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

// Todo returns generated.TodoResolver implementation.
func (r *Resolver) Todo() generated.TodoResolver { return &todoResolver{r} }

// User returns generated.UserResolver implementation.
func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type todoResolver struct{ *Resolver }
type userResolver struct{ *Resolver }
```

### DB接続とCORS設定

`server/server.go`を修正し、DBとの接続を行います。併せてCORSの設定も行います

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

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

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/MrFuku/kanban-go-nuxt-graphql/server/graph"
	"github.com/MrFuku/kanban-go-nuxt-graphql/server/graph/generated"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
	"github.com/rs/cors"

	"github.com/99designs/gqlgen/graphql/handler/transport"
	"github.com/go-chi/chi"
)

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

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)
			}
		}
	}()
	defer db.Close()
	db.LogMode(true)

	router := chi.NewRouter()
	cors := cors.New(cors.Options{
		AllowedOrigins:   []string{"*"},
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
		ExposedHeaders:   []string{"Link"},
		AllowCredentials: true,
		MaxAge:           300, // Maximum value not ignored by any of major browsers
	})
	router.Use(cors.Handler)

	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{DB: db}}))
	var mb int64 = 1 << 20
	srv.AddTransport(transport.MultipartForm{
		MaxMemory:     128 * mb,
		MaxUploadSize: 100 * mb,
	})

	router.Handle("/", playground.Handler("GraphQL playground", "/query"))
	router.Handle("/query", srv)

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

以上の作業が終わったらコンテナを立ち上げ、`http://localhost:8080/`にアクセスします
playgroundが起動しているので、クエリを試してみて問題がなければ完了です！

<img width="1436" alt="スクリーンショット 2020-05-31 15.28.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/2ab49dff-215f-6c94-1437-71280999fa1d.png">