gqlgenのチュートリアル
go run github.com/99designs/gqlgen init
でプロジェクトを初期化する。
こんな感じのディレクトリとファイルができる。
.
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│ ├── generated.go
│ ├── model
│ │ └── models_gen.go
│ ├── resolver.go
│ ├── schema.graphql
│ └── schema.resolvers.go
├── server.go
└── tools.go
schema.graphql
にスキーマを書いていくらしい。
.graphqlファイルはVSCodeの拡張を入れるとハイライトで表示してくれるようになる。
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!
}
VSCode拡張
疑問点
-
type
とinput
の違いは? -
!
とは? -
ID
はどこで宣言されている?
実際の処理はschema.resolvers.go
に書くらしい。
GraphQLのサーバーを動かすために2つのメソッドを実装しろとのこと。
We just need to implement these two methods to get our server working:
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
}
// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
panic(fmt.Errorf("not implemented: Todos - todos"))
}
メソッドの名前はschema.graphql
の定義と連動している。
type Query {
todos: [Todo!]!
}
・
・
・
type Mutation {
createTodo(input: NewTodo!): Todo!
}
まずはアプリケーションの依存関係をgraph/resolver.go
に書く。
graph/resolver.go
はserver.go
で1回だけ初期化される。
まずはtodoを保存する用のtodos
を追加する。
package graph
import "github.com/takurooo/gqlgen-todos/graph/model"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct {
todos []*model.Todo
}
処理をschema.resolvers.go
に書く。
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
randNumber, _ := rand.Int(rand.Reader, big.NewInt(100))
todo := &model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", randNumber),
User: &model.User{ID: input.UserID, Name: "user " + input.UserID},
}
r.todos = append(r.todos, todo)
return todo, nil
}
// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
return r.todos, nil
}
go run server.go
でサーバーを起動してブラウザでhttp://localhost:8080/
にアクセスするとPlaygroundが開く。
createTodo
を実行するとレスポンスが右の画面に表示される。
mutation
キーワードの後にでてくるcreateTodo
は省略可能みたい。
下記のように実行してもレスポンスが返ってきた。
queryを使うと作成したtodoが取得できる。
必要な時以外にUserがロードされるのを避けたいとのこと。
This example is great, but in the real world fetching most objects is expensive. We don’t want to load the User on the todo unless the user actually asked for it.
gqlgen.yml
でautobindを有効にしろとのこと。
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "github.com/[username]/gqlgen-todos/graph/model"
gqlgen.yml
はgqlgenのコンフィグファイル。
ここに設定を書くとコード生成の挙動を変えられる。
autobindを有効にすると自分が作成したモデル(カスタムモデル)を読み込んでくれる。カスタムモデルがあるパスを書くと有効になるみたい。
疑問点
- どんな時に
autobind
が便利なのか?
さらにTodoフィールドを追加する。
userフィールドのresolverを生成してくれるようになるらしい。
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Todo:
fields:
user:
resolver: true
どのモデルのどのフィールド化はgraph/schema.graphqls
ファイルの宣言に基づいて書かれている。
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
gqlgen.yml
に追記しなくてもTodoモデルのuserフィールドのresolverを自動で生成する場合もあった。
コンフィグのTodoのフィールドを消して、
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
graph/model/todo.go
のUser変数を削除しておく。
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
UserID string `json:"userId"`
}
でもスキーマの定義にはuser
がある。
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
この状態でgo run github.com/99designs/gqlgen generate
を実行すると、userフィールドのresolverを生成してくれる。
スキーマ定義にフィールドがあるのに、モデルに該当するフィールドにない場合、ジェネレーターがresolverを生成してくれるみたい。
graph/model/todo.go
にTodoモデルを追加する
package model
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
UserID string `json:"userId"`
User *User `json:"user"`
}
go run github.com/99designs/gqlgen generate
を再度実行する。
.
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│ ├── generated.go
│ ├── model
│ │ ├── models_gen.go
│ │ └── todo.go
│ ├── resolver.go
│ ├── schema.graphqls
│ └── schema.resolvers.go
├── server.go
└── tools.go
これまではgraph/model/models_gen.go
にTodo構造体が宣言されていたが、graph/model/models_gen.go
からTodo構造体の定義がなくなっていた。
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
type Mutation struct {
}
type NewTodo struct {
Text string `json:"text"`
UserID string `json:"userId"`
}
type Query struct {
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
autobind
によってgraph/model/todo.go
のTodo構造体が読み込まれたので自動で生成されなくなった。
この時点ではgraph/schema.graphqls
とgraph/model/todo.go
に書かれている定義が異なっているが特にエラーにはならないらしい。
クライアントが指定できるフィールドは、graph/schema.graphqls
に書かれているものだけ。
例えば定義が
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
になっていて、modelの実装が
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
UserID string `json:"userId"`
User *User `json:"user"`
}
になっている場合、クライアントはクエリでuserIdフィールドを指定できないぽい。
クライアントがクエリで指定できるのは
- id
- text
- done
- user
のみ
schema.resolvers.go
に新しいメソッドが追加されている。
// User is the resolver for the user field.
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
panic(fmt.Errorf("not implemented: User - user"))
}
CreateTodo
メソッドの実装からUser
の生成を削除して、User
メソッドにUser
の生成を追加する。
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
randNumber, _ := rand.Int(rand.Reader, big.NewInt(100))
todo := &model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", randNumber),
UserID: input.UserID,
}
r.todos = append(r.todos, todo)
return todo, nil
}
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
return &model.User{ID: obj.UserID, Name: "user " + obj.UserID}, nil
}
こうすることで、クライアントがuserフィールドを指定したときだけUser
メソッドが実行されて生成されるようになる。
これを実行するとUser
メソッドが呼ばれるが、
mutation createTodo {
createTodo(input: { text: "todo", userId: "1" }) {
text
done
user {
id
}
}
}
下記のようにuserフィールドがないとUser
メソッドが実行されない。
mutation createTodo {
createTodo(input: { text: "todo", userId: "1" }) {
text
done
}
}
最終的なディレクトリ構造
.
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│ ├── generated.go
│ ├── model
│ │ ├── models_gen.go
│ │ └── todo.go
│ ├── resolver.go
│ ├── schema.graphqls
│ └── schema.resolvers.go
├── server.go
└── tools.go
開発時に編集が必要なファイル
- スキーマの定義は
~.graphqls
に書く -
~.graphqls
ファイルから~.resolvers.go
が生成される-
foo.graphqls
というファイルがあればfoo.resolvers.go
が生成される -
~.graphqls
ファイルは複数作れる
-
- スキーマに対する処理(API処理)は
resolver.go
と~.resolvers.go
に書く-
resolver.go
は再生成されない -
~.resolvers.go
は対応する~.graphqls
が変更されたら再生成されるが、~.resolvers.go
ファイルに追加した実装はコピーされる。
-
開発時に編集が不要なファイル
-
generated.go
は編集しない -
~.graphqls
を元に生成されたモデルファイルmodels_gen.go
は編集しない- チュートリアルにあったようにカスタムモデルを作る場合はmodelパッケージにファイルを追加する