Top View


Author Yuhei Okazaki

gqlgen + EchoでgolangなGraphQLサーバを作るチュートリアル

2020/04/13

使用するライブラリ

gqlgen

2020年4月現在、Go言語でGraphQLサーバーを実装する手段として以下2つが候補に挙がります。

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!」というメッセージがブラウザに表示されます。

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
  }
}

中央の三角ボタンをクリックすると結果が取得できます。

GraphQL Mutation

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
  }
}

中央の三角ボタンをクリックすると結果が取得できます。

GraphQL Mutation

まとめ

gqlgenとEchoを組み合わせることで簡単にGraphQLサーバを実装できました。 みなさまも、ぜひお試しください。

参考文献

Yuhei Okazaki

Yuhei Okazaki

Twitter X

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。