グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



GraphQLサーバを構築するためのGoライブラリ「gqlgen」を使ってGraphQLサーバーを構築してみる

こんにちは!
今回はGoでGraphQLサーバーを作ってみたくなったのでgqlgenを利用してGraphQLサーバーを構築してみます。

GraphQLとは

GraphQLは、APIのためのクエリ言語であり、データに対して定義した型システムを使ってクエリを実行するためのサーバーサイドランタイムです。 GraphQLは特定のデータベースやストレージエンジンに縛られることなく、既存のコードやデータに裏打ちされた形で利用することができます。Introduction to GraphQL

gqlgenとは

gqlgenはGraphQLサーバを簡単に構築するためのGoライブラリです。

  • スキーマファーストのアプローチに基づいています。
  • 型安全を最優先します。
  • コードの生成を可能にします。
What is gqlgen?

GraphQLサーバーを構築してみる

gqlgenの公式にあるGetting Startedに沿ってサーバを構築してみます。

題材はTodoでtodoのリストを返す、todoを新しく作成する、終わったtodoをマークするを実装していきます。
完成版はvektah/gqlgen-tutorialsにあります

プロジェクトをセットアップします

$ mkdir gqlgen-todos
$ cd gqlgen-todos/
$ go mod init github.com/[username]/gqlgen-todos
go: creating new go.mod: module github.com/[username]/gqlgen-todos

次にtools.goを作成します。

//go:build tools
// +build tools

package tools

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

go mod tidyで依存するものをインストールします。

$ go mod tidy
go: finding module for package github.com/99designs/gqlgen
go: downloading github.com/99designs/gqlgen v0.17.20
go: found github.com/99designs/gqlgen in github.com/99designs/gqlgen v0.17.20
go: downloading github.com/urfave/cli/v2 v2.8.1
go: downloading golang.org/x/tools v0.1.12
go: downloading github.com/stretchr/testify v1.7.1
go: downloading github.com/vektah/gqlparser/v2 v2.5.1
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
go: downloading github.com/gorilla/websocket v1.5.0
go: downloading github.com/mitchellh/mapstructure v1.3.1
go: downloading github.com/agnivade/levenshtein v1.1.1
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading github.com/hashicorp/golang-lru v0.5.4
go: downloading gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.1
go: downloading github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
go: downloading github.com/russross/blackfriday/v2 v2.1.0
go: downloading github.com/kr/pretty v0.1.0
go: downloading github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48
go: downloading github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0
go: downloading golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
go: downloading github.com/kr/text v0.1.0

これで最新版のgqlgenが追加されました。

サーバーを構築します

プロジェクトの雛型を作成します。

>$ go run github.com/99designs/gqlgen init
Creating gqlgen.yml
Creating graph/schema.graphqls
Creating server.go
Generating...

Exec "go run ./server.go" to start GraphQL server

上記コマンド実行後は以下のようなディレクトリ構成になります。

$ tree 
.
├── go.mod
├── go.sum
├── gqlgen.yml             - gqlgenの構成ファイルです。生成されたコードを制御します。
├── graph
│   ├── generated          - 生成されたランタイムのみを含むパッケージ
│   │   └── generated.go
│   ├── model              - グラフモデル用のパッケージです。
│   │   └── models_gen.go
│   ├── resolver.go        - ルートグラフリゾルバタイプ。このファイルは再生成されません
│   ├── schema.graphqls    - スキーマ
│   └── schema.resolvers.go
├── server.go              - アプリのエントリーポイント
└── tools.go

次にスキーマを定義していきます。
gqlgenはスキーマファーストのライブラリなので、コードを書く前にGraphQL Schema Definition Languageを使用してAPIを記述します。

schema.graphqlsファイルに作成していきます。
ファイルを開いて見ると既にtodoの例が入っていました。

# GraphQL schema example
#
# https://gqlgen.com/getting-started/

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

次はリゾルバの実装です。 リゾルバはスキーマとデータソースを結びつける役割を持ちます。
(GraphQLの解析とリゾルバの雛形生成は、GraphQLのライブラリがやってくれます)

gqlgen の generate コマンドを実行すると、スキーマ ファイル (graph/schema.graphqls) がモデルのグラフ/モデル/* と比較され、可能な限りモデルに直接バインドされます。 これは、init が実行されたときに既に行われています。
ここでは、既に生成されたものを見てみます。

graph/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.

import (
	"context"
	"fmt"

	"github.com/Mo3g4u/gqlgen-todos/graph/generated"
	"github.com/Mo3g4u/gqlgen-todos/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 generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

CreteTodoとTodosが実装されていないとなっています。

ではまず、状態を追跡する場所が必要です。
それをgraph/resolver.goに設定します。
graph/resolver.go ファイルは、データベースなどのアプリの依存関係を宣言する場所です。
グラフを作成すると、server.go で一度初期化されます。

graph/resolver.go

package graph

import "github.com/Mo3g4u/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
}

graph/schema.resolvers.go に戻り、自動生成されたリゾルバ関数の本体を実装してみましょう。
CreateTodoについては、math.randパッケージを使用して、ランダムに生成されたIDを持つTodo を返し、それをインメモリのTodoリストに格納します。
実際のアプリでは、データベースやその他のバックエンドサービスを使用することになると思います。

graph/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.

import (
	"context"
	"math/rand"
	"fmt"

	"github.com/Mo3g4u/gqlgen-todos/graph/generated"
	"github.com/Mo3g4u/gqlgen-todos/graph/model"
)

// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
	todo := &model.Todo{
		Text: input.Text,
		ID:   fmt.Sprintf("T%d", rand.Int()),
		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
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

サーバーの実行

$ go run server.go 
2022/10/14 16:03:03 connect to http://localhost:8080/ for GraphQL playground

ブラウザにアクセスすると以下が表示されます

GraphQLのplaygroundが立ち上がったのでTodoの作成GraphQLクエリを実行してみます。

mutation createTodo {
  createTodo(input: { text: "todo", userId: "1" }) {
    user {
      id
    }
    text
    done
  }
}

次にTotosをクエリします。

query findTodos {
  todos {
    text
    done
    user {
      name
    }
  }
}

先ほど登録したtodoが表示されました。
GraphQLの特徴として、レスポンスのフィールドを指定できます。
そのためオーバフェッチ/アンダーフェッチを回避できます

オブジェクトのフェッチにはコストがかかります。
ユーザーが実際に要求しない限り、Todo にユーザーをロードしたくはありません。
そこで、生成されたTodoモデルをもう少し現実的なものに置き換えてみます。

gqlgenがカスタムモデルを見つけることができれば、それを生成するのではなく、使用できるようにするために、自動バインドを有効にします。
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/Mo3g4u/gqlgen-todos/graph/model"

また、ユーザーフィールドのリゾルバを生成するために、gqlgen.ymlにTodoフィールドのリゾルバ設定を追加します。
 Todo: から下を追加しています。

# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
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 というファイルを新規に作成します。

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"`
}

generateで再度生成します。

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

graph/schema.resolvers.go に新しいリゾルバがあるので、これを実装して CreateTodo を修正しましょう。

// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
	todo := &model.Todo{
		Text:   input.Text,
		ID:     fmt.Sprintf("T%d", rand.Int()),
		User:   &model.User{ID: input.UserID, Name: "user " + input.UserID},
		UserID: 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
}

// User is the resolver for the user field.
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

このコメントは、コードを再生成するときに実行するコマンドを go generate に伝えます。
プロジェクト全体に対して再帰的にgo generateを実行するには、このコマンドを使用します。

$ go generate ./...

サーバーを実行して動作を試します。

$ go run server.go 
2022/10/14 16:30:26 connect to http://localhost:8080/ for GraphQL playground

ここまでがGetting Startedの内容です。

gqlgenの公式には色々な情報があるのでGetting Startedを終えたらリファレンスやレシピを 読んでみると為になると思います。DatalodersでのN+1問題の解消やGinへの組み込み方法などがあります。

この記事を書いた人

アーティス
アーティス
創造性を最大限に発揮するとともに、インターネットに代表されるITを活用し、みんなの生活が便利で、豊かで、楽しいものになるようなサービスやコンテンツを考え、創り出し提供しています。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします