gqlgenチュートリアルを試す

今更な感じもあるけど、改めてgqlgenのチュートリアルをやってみる。ドキュメント通りにやればサクッと試せます。

チュートリアル

https://gqlgen.com/getting-started/ チュートリアル見ればできるので、端折って書く。ここではコメント的なことを書いておく。 チュートリアルで作るschema.graphqlは以下。

// schema.graphql
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}
type User {
  id: ID!
  name: String!
}
input NewTodo {
  text: String!
  userId: String!
}
type Query {
  todos: [Todo!]!
}
type Mutation {
  createTodo(input: NewTodo!): Todo!
}

これを元に以下コマンドでgqlgenを実行する。

$ go run github.com/99designs/gqlgen init

すると、models_gen.goにschema.graphqlで定義したものから以下の構造体が作成される。

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package gqlgen_todos
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"`
}

QueryやMutationは以下のように定義したので、resolver.goを見るとそれらの関数が定義されている。

// scheme.graphql
type Query {
  todos: [Todo!]!
}
type Mutation {
  createTodo(input: NewTodo!): Todo!
}
// resolver.go
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")
}

このresolverに作られた関数をいじってやればOK。 チュートリアルを眺めてみると、このtodoのAPIの要件としては、ユーザーのIDが渡された時だけユーザー情報を返したいらしい。 だけど、このschemaから作られるのは以下の関数。

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    panic("not implemented")
}

戻り値の*TodoのTodoはmodels_gen.goにある以下のTodo構造体なので、Userを返さないといけない。

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
    User *User  `json:"user"`
}
// Userはこれ
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

でもUser情報を持っているUser構造体はユーザーIDが渡された時にだけ返したいっぽいので、schme.graphqlで定義されたTodo構造体ではなく、別のTodo構造体を作る。resolverでは、その新たに作った構造体を使う。 追加するには、gqlgen.ymlにmodelを追加すれば使えるようになるらしいので追加。

models:
  Todo:
    model: github.com/gqlgen-todos.Todo

後はこのパスにTodo構造体を作る。

package gqlgen_todos
type Todo struct {
    ID     string
    Text   string
    Done   bool
    UserID string
}

コマンド実行

$ go run github.com/99designs/gqlgen

models_gen.goには新たに既存のTodo構造体がなくなっています。自分で新しくTodo構造体を作成したためですね。

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package gqlgen_todos
type NewTodo struct {
    Text   string `json:"text"`
    UserID string `json:"userId"`
}
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

Resolverを新たな定義によって更新するため、以下コマンドを実行

rm resolver.go
go run github.com/99designs/gqlgen

これを実行すると以下のresolverが作成されます。 最終的にAPIとして返すのは、schema.graphqlで定義したTodoだけど、resolverとして扱うのはさっき新たに作ったTodoです。

// schema.graphql
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}
// todo.go
type Todo struct {
    ID     string
    Text   string
    Done   bool
    UserID string
}

返す時の流れとしてはresolver→schema.graphqlとなるはずなので、どこかで新たに作った構造体を元にschema.graphqlのTodoに合うようにしなければなりません。 新たに作成されたresolverを見てみると以下のようになっています。

package gqlgegn_todos
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}
}
func (r *Resolver) Todo() TodoResolver {
    return &todoResolver{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")
}
type todoResolver struct{ *Resolver }
func (r *todoResolver) User(ctx context.Context, obj *Todo) (*User, error) {
    panic("not implemented")
}

注目すべきは、新たに追加されたUser関数です。

func (r *todoResolver) User(ctx context.Context, obj *Todo) (*User, error) {
    panic("not implemented")
}

このUser関数がschema.graphqlで定義したTodo構造体のフィールドUserを担ってくれるようです。自動生成されるモデルじゃなくて自分で作ったものモデル使うのもいいけど、最終時にはschema.graphqlで定義したものに合うように実装しろということですね。 generated.goをみるとinterfaceとして定義されています。

type TodoResolver interface {
    User(ctx context.Context, obj *Todo) (*User, error)
}

おそらくこのUser関数の呼び出しは自動生成されたgenerated.goでやってくれるのかと思いますが、中身を見てもどうなっているのかよくわからん。 で、チュートリアルではこんな感じになっています。

package gqlgen_todos
import (
    context "context"
    "fmt"
    "math/rand"
)
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
}

これでgo run server/server.goしてplaygroundでクエリを打ってみると結果が得られます。 自動生成されたファイルが結構色々やっちゃってくれちゃうから何をどうやっているのかは中身の実装を詳しく見ないとわからないね。