はじめに
- Go/Vue/GraphQL初学者が学習用にCRUD操作のできるアプリを作ってみた
- サーバーサイド編とフロントエンド編に分けてその手順を紹介
- ざっくりとした構成は
- サーバー(Go + gqlgen)
- フロント(Nuxt + Apollo)
- データベース(MySQL)
- ORM(gorm)
- 開発環境(docker-compose)
完成イメージ
関連リンク
免責事項
- GoもVueもそんなに知見がありません
- そのため見苦しい書きぶりをしている箇所が多々ありますがご容赦ください🙏
データベース
コンテナ情報の定義
# 適当なディレクトリにプロジェクトを作成します
❯ mkdir kanban-go-nuxt-graphql
❯ cd kanban-go-nuxt-graphql
docker-compose.yml
を作成し、dbコンテナを定義します
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;
Go(gqlgen)
ホットリロード開発環境
server/Dockerfile
を作成し、docker-compose.yml
にserverコンテナを追加します
※server
ディレクトリも作成。以後、サーバー側のコードはこの配下に置きます
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"]
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
生成されたスキーマを修正し、今回のアプリに必要な機能を定義します
# 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を使用します
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
の設定を修正します
~ 省略 ~
# 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
の作成
package util
import (
"strings"
"github.com/google/uuid"
)
func CreateUniqueID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
server/graph/resolver.go
にDBフィールド追加
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
を修正
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の設定も行います
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が起動しているので、クエリを試してみて問題がなければ完了です!