Open3

GraphQL with Go

kushidamkushidam

Why GraphQL

GraphQLとは、HTTP経由で提供されるAPIの開発規格の一種。
単一のHTTPエンドポイントにPOSTによるクエリの発行を行う。
GraphQLは、クエリの柔軟性と効率性に優れ、クライアントが必要なデータを一度のリクエストで取得できる特長がある。

一般的なAPI開発で利用されるRESTful APIは、異なるエンドポイントにサービスを設置し、さまざまなHTTPメソッド(GET、POSTなど)を使用する。

kushidamkushidam

Try GraphQL

https://gqlgen.com/getting-started/

https://github.com/kushidam/gqlgen-getting-started

  1. プロジェクト作成
mkdir gqlgen-getting-started
cd gqlgen-getting-started
go mod init github.com/[username]/gqlgen-getting-started

tools.goファイルを作成し、モジュールのツール依存関係としてgqlgenを追加する。

touch tools.go
tools.go
//go:build tools
// +build tools

package tools

import (
	_ "github.com/99designs/gqlgen"
)

依存関係を自動的に追加

go mod tidy
  1. サーバーの構築
    スケルトン作成
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つのメソッドを実装するだけで、サーバーを動作させることができる

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.resolvers.go
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でグラフを作成するときに初期化される
server.go
type Resolver struct{
	todos []*model.Todo
}

graph/schema.resolvers.goに自動生成されたリゾルバ関数の本体を実装する。

  • CreateTodo
    crypto.randパッケージを使用して、ランダムに生成されたIDを持つTodoを返し、それをインメモリのTodoリストに格納する。
graph/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),
		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にブラウザでアクセスしてクエリを叩いてみる

クエリ例1
mutation createTodo {
  createTodo(input: { text: "todo", userId: "1" }) {
    user {
      id
    }
    text
    done
  }
}
クエリ例2
query findTodos {
  todos {
    text
    done
    user {
      name
    }
  }
}

とはいえ、現実ではほとんどのオブジェクトのフェッチにはコストがかかる。
ユーザーが実際に要求しない限り、ロードしたくない。
そこで、生成されたTodoモデルをもう少し現実的なものに置き換えてる。
"autobind" を有効にすることで、必要な情報が実際に要求されたときにのみ読み込むことが可能になる。

  • オートバインドを有効にする
  • gqlgenがカスタムモデルを生成するのではなく、カスタムモデルを見つけることができればそれを使うようになる
  • gqlgen.ymlautobind設定行をコメント解除する
gqlgen.yml
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
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の一番上、packageimportの間に以下の行を追加

//go:generate go run github.com/99designs/gqlgen generate

このコメントは、コード生成プロセスを手動でトリガーできるようになる。
これは"gqlgen" パッケージを実行し、GraphQLのスキーマ定義から関連するGoコードを生成する。

プロジェクト全体で再帰的にgo generateを実行するには、下記コマンド

go generate ./...