GraphQL with Go

Why GraphQL
GraphQLとは、HTTP経由で提供されるAPIの開発規格の一種。
単一のHTTPエンドポイントにPOSTによるクエリの発行を行う。
GraphQLは、クエリの柔軟性と効率性に優れ、クライアントが必要なデータを一度のリクエストで取得できる特長がある。
一般的なAPI開発で利用されるRESTful APIは、異なるエンドポイントにサービスを設置し、さまざまなHTTPメソッド(GET、POSTなど)を使用する。

Try GraphQL
- プロジェクト作成
mkdir gqlgen-getting-started
cd gqlgen-getting-started
go mod init github.com/[username]/gqlgen-getting-started
tools.goファイルを作成し、モジュールのツール依存関係としてgqlgenを追加する。
touch tools.go
//go:build tools
// +build tools
package tools
import (
_ "github.com/99designs/gqlgen"
)
依存関係を自動的に追加
go mod tidy
- サーバーの構築
スケルトン作成
go run github.com/99designs/gqlgen init
推奨されるパッケージ・レイアウトが作成される
├── go.mod
├── go.sum
├── gqlgen.yml - gqlgenの設定ファイル、生成されるコードを制御するためのノブ。
├── graph
│ ├── generated - 生成されたランタイムのみを含むパッケージ。
│ │ └── generated.go
│ ├── model -すべてのグラフモデルに対応するパッケージ。
│ │ └── models_gen.go
│ ├── resolver.go - このファイルは再生成されない
│ ├── schema.graphqls - スキーマ。スキーマは好きなだけ .graphql ファイルに分割できる。
│ └── schema.resolvers.go - schema.graphqlのリゾルバ実装。
└── server.go - アプリのエントリーポイント。好きなようにカスタマイズ
スキーマの定義
schema.graphqls
にデフォルトで定義されているが、好きなだけ異なるファイルに分割することができる。
schema.graphqls
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!
}
リゾルバの実装
リゾルバ(Resolver)はクエリのフィールドごとにデータを取得または処理するための関数
リゾルバの主な役割:
- データの取得
データベース、API、外部サービスなどからデータを取得するためのクエリを実行し、結果を返す。 - データの変換
取得したデータを必要なフォーマットに変換し、クライアントに応答する。 - ビジネスロジックの実行
特定のクエリやミューテーションに対して特別なロジックを実行するために使用。ユーザーの認証や権限の確認を行うことがある。
この2つのメソッドを実装するだけで、サーバーを動作させることができる
// 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"))
}
全体
package graph
// 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.
// Code generated by github.com/99designs/gqlgen version v0.17.40
import (
"context"
"fmt"
"github.com/kushidam/gqlgen-getting-started/graph/model"
)
// 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"))
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
graph/resolver.go
に記述する。
-
graph/resolver.go
は、DBのようなアプリの依存関係を宣言する場所 -
server.go
でグラフを作成するときに初期化される
type Resolver struct{
todos []*model.Todo
}
graph/schema.resolvers.go
に自動生成されたリゾルバ関数の本体を実装する。
-
CreateTodo
crypto.rand
パッケージを使用して、ランダムに生成されたIDを持つTodoを返し、それをインメモリのTodoリストに格納する。
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
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
return r.todos, nil
}
最低限の実装完了
実際に起動してみる
go run server.go
20YY/MM/DD hh:mm:ss connect to http://localhost:8080/ for GraphQL playground
http://localhost:8080
にブラウザでアクセスしてクエリを叩いてみる
mutation createTodo {
createTodo(input: { text: "todo", userId: "1" }) {
user {
id
}
text
done
}
}
query findTodos {
todos {
text
done
user {
name
}
}
}
とはいえ、現実ではほとんどのオブジェクトのフェッチにはコストがかかる。
ユーザーが実際に要求しない限り、ロードしたくない。
そこで、生成されたTodoモデルをもう少し現実的なものに置き換えてる。
"autobind" を有効にすることで、必要な情報が実際に要求されたときにのみ読み込むことが可能になる。
- オートバインドを有効にする
-
gqlgen
がカスタムモデルを生成するのではなく、カスタムモデルを見つけることができればそれを使うようになる -
gqlgen.yml
のautobind
設定行をコメント解除する
autobind:
- "github.com/[username]/gqlgen-todos/graph/model"
ユーザーフィールドのリゾルバを生成するために、gqlgen.yml
に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
# 下記四行追加
Todo:
fields:
user:
resolver: true
graph/model/todo.go
という新しいファイルを作成する。
touch 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
graph/schema.resolvers.go
に新しいリゾルバがあるので、CreateTodoを追加で実装する。
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
}
仕上げ
resolver.go
の一番上、package
とimport
の間に以下の行を追加
//go:generate go run github.com/99designs/gqlgen generate
このコメントは、コード生成プロセスを手動でトリガーできるようになる。
これは"gqlgen" パッケージを実行し、GraphQLのスキーマ定義から関連するGoコードを生成する。
プロジェクト全体で再帰的にgo generate
を実行するには、下記コマンド
go generate ./...