gqlgenでGraphQL serverをGoで構築する
golang
Lastmod: 2020-10-19

gqlgen

gqlgen は Go の GraphQL ライブラリで、GraphQL をインタフェースとして持つ API サーバを Go で構築できる。

gqlgen はコード上に GraphQL スキーマをガシガシ書いていくライブラリとは異なり、
GraphQL ファイルに記述するスキーマ情報からコードを自動生成する。
なのでコードがごちゃごちゃしにくく、Go を知らない人でも GraphQL スキーマを書ける人なら誰でも定義を編集できるメリットがある(と個人的に考えている)。

デメリットとしては、自動生成されるコードに関してはブラックボックスになってしまうということと、
読み解いて編集したとてスキーマ更新時に再編集しなければいけないところだろうか。

Usage

プロジェクトの作成

公式のチュートリアルがあり一回通してみたが、これとは別に自分なりにプロジェクトを再構築した。

$ mkdir tmp-gqlgen; cd tmp-gqlgen
$ go mod init (Gov1.11以上であれば)

まずはパッケージを入手。

$ go get github.com/99designs/gqlgen

次にプロジェクトルートに GraphQL スキーマファイルを用意。

schema.graphql

type Query {
  user(id: ID): User!
  pet(id: ID): Pet!
}

type User {
  id: ID
  name: String
}

type Pet {
  id: ID
  name: String
}

次に gqlgen.yml を用意する。
このファイルは gqlgen でコードを自動生成する際に必要になる。

gqlgen.yml の詳しい定義はこちら

# GraphQLスキーマファイルの場所
schema:
  - ./*.graphql

# スキーマGo実装ファイルの生成場所
exec:
  filename: graph/generated/generated.go
  package: generated

# モデル構造体ファイルの生成場所
model:
  filename: graph/model/models_gen.go
  package: model

# resolver(GraphQL版controller的なもの)ファイルの生成場所
resolver:
  layout: follow-schema
  dir: graph/resolver
  package: resolver

# 不足したスキーマ構造体を自動生成する場所
autobind:
  - 'github.com/rennnosuke/tmp-gqlgen/graph/model'

gqlgen.yml が用意できたら、コードの自動生成を実行する。

$ go run github.com/99designs/gqlgen

すると yml で指定したものを含めいくつかコードが生成される。
ファイル名が気に入らなければ、yml 上で変更できる。

tmp-gqlgen
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│   ├── generated
│   │   └── generated.go
│   ├── model
│   │   └── models_gen.go
│   └── resolver
│       ├── resolver.go
│       └── schema.resolvers.go
├── schema.graphql
└── server.go

models_gen.go

スキーマに定義した Type に対応する構造体が定義された。
コメントにあるように、自動生成ファイルはいじらないほうが吉。

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package model

type Pet struct {
	ID   *string `json:"id"`
	Name *string `json:"name"`
}

type User struct {
	ID   *string `json:"id"`
	Name *string `json:"name"`
}

resolver.go

ベースになる resolver。
resolver は Web アプリケーションアーキテクチャとしての MVC における Controller に近く、endpoint に対応するメソッドを実装している。余談だが、GraphQL モジュール自体薄く保つべきという指針が提唱されているので、Controller 同様多くの処理は持たせず軽い Validation などに留めるのがよい。

package resolver

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

type Resolver struct{}

schema.resolver.go

yml の resolver に指定したスキーマファイル分だけ生成される。今回は schema.graphql のみ指定したので、 schema.resolver.go ファイル1つが生成された。

これらの自動生成ファイルで定義される xxxResolver 構造体は resolver.goResolver 構造体をコンポジットする。

QueryResolver の持つメソッドは GraphQL スキーマクエリのメソッドに対応するが、実装は空(panic)になっているため、独自に編集する必要がある(ので、もちろん編集は可能)。

package resolver

// 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"
	"fmt"

	"github.com/rennnosuke/tmp-gqlgen/graph/generated"
	"github.com/rennnosuke/tmp-gqlgen/graph/model"
)

func (r *queryResolver) User(ctx context.Context, id *string) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Pet(ctx context.Context, id *string) (*model.Pet, error) {
	panic(fmt.Errorf("not implemented"))
}

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

type queryResolver struct{ *Resolver }

スキーマの更新・再生成

一旦初期自動生成後、スキーマの変更を反映したい場合は以下を実行する。

$ go run github.com/99designs/gqlgen

これで resolverの上書きなしに、 modelexec に該当するファイルだけ更新できる。

サーバーの起動

コード自動生成の際、GraphQL API サーバーを起動する server.go も自動で生成されている。

package main

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

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/rennnosuke/tmp-gqlgen/graph/generated"
	"github.com/rennnosuke/tmp-gqlgen/graph/resolver"
)

const defaultPort = "8080"

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver.Resolver{}}))

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

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

ちなみに、 Server 構造体が Go 標準 net/httpHandler インタフェース実装のハンドラ関数 Handler を持っているため、既存プロジェクトでも net/http を使用していればすぐに組み込むことができる。

server.go をそのまま起動すると API サーバーを起動できる。

$ go run server.go
2020/05/17 13:19:52 connect to http://localhost:8080/ for GraphQL playground

Request

実際にクエリを投げられるのを確認するため、Resolver メソッドを書き換える。
構造体のすべてのメンバ型がポインタなので、値を代入するとき少しもどかしい、、、

schema.resolver.go

func (r *queryResolver) User(ctx context.Context, id *string) (*model.User, error) {
	name := "Bob"
	return &model.User{
		ID:   id,
		Name: &name,
	}, nil
}

上記実装後、 go run server.go でサーバーを再起動すると user() クエリが投げられるようになる。

Query

{
  user(id:"user::1"){
    name
  }
}

Response

{
  "data": {
    "user": {
      "name": "Bob"
    }
  }
}

備考

個人の見解だが、自動生成される部分(特に exec に該当する部分)は殆ど変更の入らない部分なのと、 model resolver も自動生成 + 取得処理の外部モジュール化で十分だと思ったので、Go で GraphQL API サーバーを実装する際は gqlgen で良きかな、と思った。

graphql-goも試したが、Resolver 周りの実装が煩雑だったので心が折れた)

参考文献

gqlgen - Github