GraphQLのモデルからファイルを自動生成してくれるgqlgenというものがあります。 https://github.com/99designs/gqlgen
挙動を理解するために チュートリアル に沿って使ってみました。
Golang ってコードの自動生成多いですね。ようやく慣れてきました。
Initialize
このセクションのコードはこのあとのセクションと区別するために
world/
ディレクトリ配下で行います。
まずは以下のファイルを置いて
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: String!
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
以下のコマンドでファイル群を初期化します。
$ go run github.com/99designs/gqlgen init
作られたファイルが以下の5つです。
- gqlgen.yml
- 自動生成の設定ファイル
gqlgen.yml
# .gqlgen.yml example # # Refer to https://gqlgen.com/config/ # for detailed .gqlgen.yml documentation. schema: - schema.graphql exec: filename: generated.go model: filename: models_gen.go resolver: filename: resolver.go type: Resolver autobind: []
-
- schema
- GraphQL の設定ファイルです。
- 配列で複数指定することもできますが、 Glob で指定できるため
schema/*.graphql
のようにディレクトリ配下の設定を一括取り込みしたりもできます。
-
- exec
- 後述する models_gen.go
をどのようなファイル名、どのようなパッケージ名で出力するか設定できます。
filename
にはパスから指定できるのでディレクトリも変更できます。package
を指定すれば出力するファイルのパッケージ名を変更できます。- 出力するディレクトリ名と合わせる必要があります
- 後述する models_gen.go
をどのようなファイル名、どのようなパッケージ名で出力するか設定できます。
-
- model
- 後述する generated.go をどのようなファイル名、どのようなパッケージ名で出力するか設定できます。 (設定方法は execと同じため省略)
-
- models
- 手動で生成したモデルを gqlgen に教えてあげるための設定です。
{親ディレクトリまでのパス}/{パッケージ名}.{モデル名}
という具合に指定します。 詳しくは次のセクションまで読み進めてください。
- 手動で生成したモデルを gqlgen に教えてあげるための設定です。
-
- resolver
- 後述する resolver.go をどのようなファイル名、どのようなパッケージ名で出力するか設定できます。 (設定方法は execと同じため省略)
- type はリゾルバに設定する名前です。変えたからと言って挙動が変わったりはしません。
通常は
Resolver
のままにしておくのがよいでしょう。
-
- autobind
- 手動で作成したモデルを自動的に読み込むための設定です。 配列でパスを指定すれば該当するモデルを読み込んでくれます。
v0.9.1
から使えます。- How to configure gqlgen using gqlgen.yml - gqlgen
- 自動生成の設定ファイル
- generated.go
- GraphQL を処理するためのランタイム。 gqlgen 自身はコードを自動生成するだけでランタイムを提供しません。
- このファイルが置かれたディレクトリをモジュールとしてインポートして扱うことになります。手動で変更してはいけません。
ResolverRoot
というインタフェースに意図しないメソッドが出力される場合、抽出すべきフィールドの型が誤っている可能性があります。 (前にハマったのでメモ)- 今回
world/generated.go
というファイルが出力されましたが、ファイルが大きすぎてページの読み込みに支障が出ているのでのせません。
- models_gen.go
- graphql の モデル定義を Golang で扱えるように構造体に変換したコード。手動で変更してはいけません。
-
models_gen.go
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package world type NewTodo struct { Text string `json:"text"` UserID string `json:"userId"` } type Todo struct { ID string `json:"id"` Text string `json:"text"` Done bool `json:"done"` User *User `json:"user"` } type User struct { ID string `json:"id"` Name string `json:"name"` }
- resolver.go
- 実際にリクエストを処理するコードです。初期状態では関数のガワだけが定義されており呼び出すとパニックが発生するので徐々に実装していきます。
-
resolver.go
package world import ( "context" ) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. type Resolver struct{} func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) { panic("not implemented") } type queryResolver struct{ *Resolver } func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) { panic("not implemented") }
- server/server.go
- GraphQLサーバを動かすための最小限のエンドポイント実装です。
-
server.go
package main import ( "hello/world" "log" "net/http" "os" "github.com/99designs/gqlgen/handler" ) const defaultPort = "8080" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } http.Handle("/", handler.Playground("GraphQL playground", "/query")) http.Handle("/query", handler.GraphQL(world.NewExecutableSchema(world.Config{Resolvers: &world.Resolver{}}))) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }
早速サーバを起動してみましょう。
$ go run server/server.go
2020/02/09 21:00:00 connect to http://localhost:8080/ for GraphQL playground
続いて localhost:8080 にアクセスしてみると、Play groundが表示されました。
まだ実装されていないので何をやってもエラーになりますが、とりあえず一歩は進展しました。
Implement
- info
- 前のセクションと区別するために
kitty/
ディレクトリ配下で行います。 - ちなみにキティはよく知りません。
- 前のセクションと区別するために
チュートリアルに沿って話を進めていきます。
先程自動生成されたファイルは正しくなかったとしましょう。
Todo
構造体には User
構造体が埋め込まれていましたが、実は UserID だけで十分だったのです。
models_gen.go
に定義された Todo を切り出して
todo.go
に以下のように記述します。
package kitty
type Todo struct {
ID string
Text string
Done bool
UserID string
}
(「切り出して」とは言いましたが、 models_gen.go を編集する必要はありません。どうせこのあと作り直すのでどちらでもよいですが)
設定ファイルも次のようにいじっていきます。
schema:
- schema.graphql
exec:
filename: generated.go
model:
filename: models_gen.go
models:
Todo:
model: hello/kitty.Todo
resolver:
filename: resolver.go
type: Resolver
autobind: []
Todo モデルは手動で作ったからそれを見てくれという指定を
models
に追加しただけです。
- info
- 今回はひとつなのであまり気になりませんが、モデルが増えてくると管理が煩雑になってきます。
- 手動で追加したのに gqlgen.yml に登録し忘れることもあるでしょう。
- 先程の説明で軽く触れましたが
autobind
を使うとモデルを自動検出して読み込んでくれます。 つまり、 models の代わりに以下のように autobind を書いても同じことです。 -
autobind: - hello/kitty
ではもう一度コードの生成を行います。
kitty ディレクトリに移動して
kitty $ go run github.com/99designs/gqlgen -v
kitty/todo.go:3 adding resolver method for Todo.user, nothing matched
kitty/resolver.go already exists
- info
- 設定ファイルは
--config=
で指定できるため、ファイル名を変える場合はこのオプションとともに利用ください。 -v
は詳細情報表示のオプションです
- 設定ファイルは
generated.go と models_gen.go が修正されました。 (v0.9.0以前では、フィールドのGetterを書かないとパニックになることがあったのですが、現時点の最新v0.10.2では発生しないようです)
先程のコードを比較してみると models_gen.go から Todo モデルが消えているのがわかります。
-
world/models_gen.go
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package world type NewTodo struct { Text string `json:"text"` UserID string `json:"userId"` } type Todo struct { ID string `json:"id"` Text string `json:"text"` Done bool `json:"done"` User *User `json:"user"` } type User struct { ID string `json:"id"` Name string `json:"name"` }
-
kitty/models_gen.go
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package kitty type NewTodo struct { Text string `json:"text"` UserID string `json:"userId"` } type User struct { ID string `json:"id"` Name string `json:"name"` }
todo.go
に手動で定義したのでこれは期待通りの動作です。むしろ作られたら困ります。
先程の生成で「 resolver.go
がすでにあるので生成されなかった」と警告が出ていたのに気づきましたか?
resolver.go は編集可能なファイルなので勝手に上書きしたりしません。 作り直す場合は一度削除して再実行します。
kitty $ rm resolver.go
kitty $ go run github.com/99designs/gqlgen
少しわかりにくいですが、 TodoResolver
タイプと
User
メソッドが追加されました。
- warning
- お気づきのように Query や Mutation はインタフェースのフィールドに追加されます。
- resolver.go を新規で作る場合はこれらのメソッドも空で作られてくれますが、 すでにある場合は開発者が resolver.go に手動でメソッドを追加してあげないとエラーになるので注意しましょう。
-
type MutationResolver interface { CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) } type QueryResolver interface { Todos(ctx context.Context) ([]*Todo, error) }
ここに処理を実装していきましょう。
package kitty
import (
"context"
"fmt"
"math/rand"
) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
type Resolver struct {
todos []*Todo
}
func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
func (r *Resolver) Todo() TodoResolver {
return &todoResolver{r}
}
type mutationResolver struct{ *Resolver }
func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
todo := &Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", rand.Int()),
UserID: input.UserID,
}
r.todos = append(r.todos, todo)
return todo, nil
}
type queryResolver struct{ *Resolver }
func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
return r.todos, nil
}
type todoResolver struct{ *Resolver }
func (r *todoResolver) User(ctx context.Context, obj *Todo) (*User, error) {
return &User{ID: obj.UserID, Name: "user " + obj.UserID}, nil
}
以下のように変更して再度サーバを起動します。
- Resolver は Todo を保存するための Array
todos
を持つ- Resolver を埋め込んだ全ての構造体も同様に todos を参照できる
- CreateTodo は 受け取ったパラメータをもとに Todo を作成し todos に追加する
- Todos は todos をそのまま返却する
動作確認に Mutation(x2) と Query を実行します。
- Mutation1
-
- Mutation2
-
- Query
-
うまく動いているようでよかったです。
ちなみに gqlgen とは直接関係ありませんが、GraphQL Playground はサーバ側の負荷が高いとうんともすんとも言わなくなることがあるので
apollo-client-developer
という Chrome アドオンを使っています。
実際のGraphQLリクエストを捕捉して再送できたりするので便利です。
https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm
履歴を永続化してくれるともっとうれしいんだけど。