【GraphQL】gqlgenでGraphQL serverをGoで構築する
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
でコードを自動生成する際に必要になる。
# 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.go
の Resolver
構造体をコンポジットする。
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
の上書きなしに、 model
や exec
に該当するファイルだけ更新できる。
サーバーの起動
コード自動生成の際、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/http
の Handler
インタフェース実装のハンドラ関数 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 周りの実装が煩雑だったので心が折れた)