Table of Contents
使用するライブラリ
gqlgen
2020年4月現在、Go言語でGraphQLサーバーを実装する手段として以下2つが候補に挙がります。
- graphql-go/graphql (Star 6.2k)
- 99designs/gqlgen (Star 4.2k)
Star数では前者が優勢ですし日本語の情報も得やすいのですが、以下のような強みに期待して今回は後者を使いました。
- スキーマファーストである
- コードを自動生成してくれるのでロジックの実装に注力できる
- フロントエンドとのスキーマの共有が容易
- 前者よりも開発が活発である
Echo
Go言語のWebフレームワークです。Go言語だと簡単なWebサーバは net/httpでも作れますし、gqlgenも単体でWebサーバとしての機能を持っています。Echo以外にもGo言語製のWebフレームワークは多数存在しています。
その中でも、「Webアプリケーションに必要な一通りの機能」を有しているのがEchoです。今後、JWT認証であったり諸々のセキュリティ対策だったりを追加することを想定して、Echoを選択しました。
その他
ORMとして gorm を、マイグレーションツールとして goose を使用します。
シンプルWebサーバを実装
必要なライブラリをgo get
必要なライブラリをgo getします。
Go Modulesを使っているので、環境変数 GO111MODULE
のexportを忘れないよう注意してください。
$ export GO111MODULE=on
$ go mod init github.com/yuuu/gqlgen-echo-sample
$ go get bitbucket.org/liamstask/goose
$ go get github.com/99designs/gqlgen
$ go get github.com/labstack/echo
$ go get github.com/jinzhu/gorm
Echoの実装
main.go
を作成して以下のコードを書きます。
package main
import (
"log"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", welcome())
err := e.Start(":3000")
if err != nil {
log.Fatalln(err)
}
}
func welcome() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome!")
}
}
サーバを起動するため go run
します。
$ go run main.go
http://localhost:3000 へアクセスすると「Welcome!」というメッセージがブラウザに表示されます。
gooseを使ったマイグレーション
DBとの接続のためにマイグレーションファイルを作成します。
事前にPostgreSQLにログインして以下のクエリを実行し、データベースを作成しておきましょう。
CREATE DATABASE "gqlgen-echo-sample";
このデータベースと接続するため、 db/dbconfig.yml
を作成し以下の通り記述します。
development:
driver: postgres
open: user={{PostgreSQLのユーザ名}} password={{PostgreSQLのパスワード}} dbname=gqlgen-echo-sample sslmode=disable
データベースとの接続を確認するため以下コマンドを実行します。同様に出力が得られれば接続成功です。
$ goose status
goose: status for environment 'development'
Applied At Migration
=======================================
次にマイグレーションファイルを作成します。
$ goose create CreateTasks sql
作成されたファイルを以下のように編集します。
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE tasks (
id SERIAL NOT NULL,
title varchar(255) DEFAULT NULL,
note text DEFAULT NULL,
completed integer DEFAULT 0,
created_at TIMESTAMP DEFAULT NULL,
updated_at TIMESTAMP DEFAULT NULL,
PRIMARY KEY(id)
);
CREATE INDEX task_id on tasks (id);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP INDEX task_id;
DROP TABLE tasks;
以下コマンドを実行し、マイグレーションを実行します。
$ goose up
goose: migrating db environment 'development', current version: 0, target: 20200413055140
OK 20200413055140_CreateTasks.sql
gormの導入
main.go
の冒頭部分にDBとの接続処理を追加します。
package main
import (
"fmt"
"log"
"net/http"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
_, err := gorm.Open(
"postgres",
fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s password=%s sslmode=disable",
"127.0.0.1", 5432, "{{PostgreSQLのユーザ名}}", "gqlgen-echo-sample", "{{PostgreSQLのパスワード}}",
),
)
if err != nil {
log.Fatalln(err)
}
e := echo.New()
// 省略
}
// 省略
GraphQLを実装
Mutationを実装
まずはじめにgqlgenのジェネレータを実行します。
$ gqlgen init
サンプルのGraphQLスキーマとそれを実現するソースコードが生成されます。
GraphQLスキーマ( graph/schema.graphqls
)を以下のように修正します。
type Task {
id: ID!
title: String!
note: String!
completed: Int!
created_at: String!
updated_at: String!
}
input NewTask {
title: String!
note: String!
}
type Mutation {
createTask(input: NewTask!): Task!
}
スキーマを修正したのでソースコードを再生成します。
$ rm graph/schema.resolvers.go
$ gqlgen
Mutationに相当する処理を 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"
"time"
"github.com/yuuu/gqlgen-echo-sample/graph/generated"
"github.com/yuuu/gqlgen-echo-sample/graph/model"
)
func (r *mutationResolver) CreateTask(ctx context.Context, input model.NewTask) (*model.Task, error) {
// ここから追記
timestamp := time.Now().Format("2006-01-02 15:04:05")
task := model.Task{
Title: input.Title,
Note: input.Note,
Completed: 0,
CreatedAt: timestamp,
UpdatedAt: timestamp,
}
r.DB.Create(&task)
return &task, nil
// ここまで追記
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
type mutationResolver struct{ *Resolver }
Resolverのメソッドでgormを使えるようにするため、 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 // ここを追記
}
Echoとの接続
実装したMutationとEchoを接続するため、 main.go
へ追記します。
package main
import (
"fmt"
"log"
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/yuuu/gqlgen-echo-sample/graph"
"github.com/yuuu/gqlgen-echo-sample/graph/generated"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
db, err := gorm.Open( // 修正
"postgres",
fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s password=%s sslmode=disable",
"127.0.0.1", 5432, "postgres", "gqlgen-echo-sample", "postgres",
),
)
if err != nil {
log.Fatalln(err)
}
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", welcome())
// 追記ここから
graphqlHandler := handler.NewDefaultServer(
generated.NewExecutableSchema(
generated.Config{Resolvers: &graph.Resolver{DB: db}},
),
)
playgroundHandler := playground.Handler("GraphQL", "/query")
e.POST("/query", func(c echo.Context) error {
graphqlHandler.ServeHTTP(c.Response(), c.Request())
return nil
})
e.GET("/playground", func(c echo.Context) error {
playgroundHandler.ServeHTTP(c.Response(), c.Request())
return nil
})
// 追記ここまで
err = e.Start(":3000")
if err != nil {
log.Fatalln(err)
}
}
func welcome() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome!")
}
}
最後に、gqlgenが生成した server.go
は今回不要なので削除しておきましょう。
$ rm server.go
Mutationの動作確認
サーバを起動します。
$ go run main.go
ブラウザで http://localhost:3000/playground へアクセスします。
試しに以下のMutationを実行してみましょう。
mutation {
createTask(
input: {
title: "Title",
note: "Note..."
}
) {
id
title
note
completed
created_at
updated_at
}
}
中央の三角ボタンをクリックすると結果が取得できます。
Queryを実装
スキーマへ追記します。
type Task {
id: ID!
title: String!
note: String!
completed: Int!
created_at: String!
updated_at: String!
}
input NewTask {
title: String!
note: String!
}
type Mutation {
createTask(input: NewTask!): Task!
}
# ここから追記
type Query {
tasks: [Task!]!
}
# ここまで追記
スキーマを修正したのでコードを再生成します。
$ gqlgen
Queryに相当する処理を 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"
"time"
"github.com/yuuu/gqlgen-echo-sample/graph/generated"
"github.com/yuuu/gqlgen-echo-sample/graph/model"
)
// 省略
func (r *queryResolver) Tasks(ctx context.Context) ([]*model.Task, error) {
// ここから追記
tasks := []*model.Task{}
r.DB.Find(&tasks)
return tasks, 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} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Queryの動作確認
再び、サーバを起動します。
$ go run main.go
ブラウザで http://localhost:3000/playground へアクセスします。
試しに以下のQueryを実行してみましょう。
{
tasks {
id
title
note
completed
created_at
updated_at
}
}
中央の三角ボタンをクリックすると結果が取得できます。
まとめ
gqlgenとEchoを組み合わせることで簡単にGraphQLサーバを実装できました。 みなさまも、ぜひお試しください。