99
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-07

はじめに

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

完成イメージ

タイトルなし.gif
※ タスクの並び順は操作できません

関連リンク

免責事項

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

データベース

コンテナ情報の定義

# 適当なディレクトリにプロジェクトを作成しますmkdir kanban-go-nuxt-graphql
❯ cd kanban-go-nuxt-graphql

docker-compose.ymlを作成し、dbコンテナを定義します

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を実行しテーブルを作成する

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

Go(gqlgen)

ホットリロード開発環境

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

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"]
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の作成

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

# go.mod ファイルの作成
❯ go mod init

これでホットリロードの開発ができる環境が整いました

gqlgelスキーマ定義

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

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

# ファイルの自動生成
❯ go run github.com/99designs/gqlgen init

生成されたスキーマを修正し、今回のアプリに必要な機能を定義します

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を使用します

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の設定を修正します

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

定義した内容を元に、再度ファイルを自動生成

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

リゾルバーの実装

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

server/util/util.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フィールド追加

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を修正

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の設定も行います

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が起動しているので、クエリを試してみて問題がなければ完了です!

スクリーンショット 2020-05-31 15.28.27.png
99
93
3

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
99
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?