Open14

gqlgenのチュートリアル

takuroootakurooo

https://gqlgen.com/

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
takuroootakurooo

schema.graphqlにスキーマを書いていくらしい。
.graphqlファイルはVSCodeの拡張を入れるとハイライトで表示してくれるようになる。

schema.graphql
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拡張
https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax

疑問点

  • typeinputの違いは?
  • !とは?
  • IDはどこで宣言されている?
takuroootakurooo

実際の処理はschema.resolvers.goに書くらしい。
GraphQLのサーバーを動かすために2つのメソッドを実装しろとのこと。

We just need to implement these two methods to get our server working:

schema.resolvers.go
// 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の定義と連動している。

schema.graphql
type Query {
  todos: [Todo!]!
}
・
・
・
type Mutation {
  createTodo(input: NewTodo!): Todo!
}
takuroootakurooo

まずはアプリケーションの依存関係をgraph/resolver.goに書く。
graph/resolver.goserver.goで1回だけ初期化される。
まずはtodoを保存する用のtodosを追加する。

graph/resolver.go
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
}
takuroootakurooo

処理をschema.resolvers.goに書く。

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
}
takuroootakurooo

go run server.goでサーバーを起動してブラウザでhttp://localhost:8080/にアクセスするとPlaygroundが開く。

takuroootakurooo

createTodoを実行するとレスポンスが右の画面に表示される。

mutationキーワードの後にでてくるcreateTodoは省略可能みたい。
下記のように実行してもレスポンスが返ってきた。

takuroootakurooo

必要な時以外に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.yml
# 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が便利なのか?
takuroootakurooo

さらにTodoフィールドを追加する。
userフィールドのresolverを生成してくれるようになるらしい。

gqlgen.yml
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ファイルの宣言に基づいて書かれている。

graph/schema.graphqls
type Todo {
    id: ID!
    text: String!
    done: Boolean!
    user: User!
}

gqlgen.ymlに追記しなくてもTodoモデルのuserフィールドのresolverを自動で生成する場合もあった。

コンフィグのTodoのフィールドを消して、

gqlgen.yml
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変数を削除しておく。

graph/model/todo.go
type Todo struct {
	ID     string `json:"id"`
	Text   string `json:"text"`
	Done   bool   `json:"done"`
	UserID string `json:"userId"`
}

でもスキーマの定義にはuserがある。

graph/schema.graphqls
type Todo {
    id: ID!
    text: String!
    done: Boolean!
    user: User!
}

この状態でgo run github.com/99designs/gqlgen generateを実行すると、userフィールドのresolverを生成してくれる。
スキーマ定義にフィールドがあるのに、モデルに該当するフィールドにない場合、ジェネレーターがresolverを生成してくれるみたい。

takuroootakurooo

graph/model/todo.goにTodoモデルを追加する

graph/model/todo.go
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構造体の定義がなくなっていた。

graph/model/models_gen.go
// 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構造体が読み込まれたので自動で生成されなくなった。

takuroootakurooo

この時点ではgraph/schema.graphqlsgraph/model/todo.goに書かれている定義が異なっているが特にエラーにはならないらしい。

クライアントが指定できるフィールドは、graph/schema.graphqlsに書かれているものだけ。
例えば定義が

graph/schema.graphqls
type Todo {
    id: ID!
    text: String!
    done: Boolean!
    user: User!
}

になっていて、modelの実装が

graph/model/todo.go
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
    のみ
takuroootakurooo

schema.resolvers.goに新しいメソッドが追加されている。

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の生成を追加する。

schema.resolvers.go
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
  }
}
takuroootakurooo

最終的なディレクトリ構造

.
├── 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パッケージにファイルを追加する