😽

gqlgenチュートリアルをできるだけわかりやすく解説する

2022/05/02に公開

はじめに

エンジニアインターン2社目にしてGolang+GraphQLで開発することになりました。
最近こそ理解が進みはしたものの、今までREST以外の世界を知らなかった自分にとってgqlgenが何をしてくれるのか、何が便利なのかピンと来なかったので同じような境遇の方のために記事を書こうと思います。

前提知識

  • GraphQLの基本がわかる
  • Golangの基本文法がわかる

gqlgenとは?

公式から引用

gqlgen is a Go library for building GraphQL servers without any fuss.
・gqlgen is based on a Schema first approach — You get to Define your API using the GraphQL Schema Definition Language.
・gqlgen prioritizes Type safety — You should never see map[string]interface{} here.
・gqlgen enables Codegen — We generate the boring bits, so you can focus on building your app quickly.

要はGraphQLサーバーを作る際に「スキーマファースト」で「型安全性を保ったまま」「コードを自動生成」できるGolangのライブラリというわけです。

目標

gqlgenチュートリアルに沿って簡単なtodoアプリケーションを作成します。挙動はGraphQL Playroundで確認するような形にします。

今回のコード
https://github.com/omoterikuto/gqlgen_tutorial

Let's start!

まずは準備から。作業ディレクトリを作成してgoの開発環境を整えましょう。今回はgolang 1.18を使用します。

mkdir gqlgen_tutorial && cd gqlgen_tutorial
go mod init gqlgen_tutorial

次に今回の主役gqlgenパッケージとその依存関係含めダウンロードします。

go get -u github.com/99designs/gqlgen@v0.17.5

こだわりはありませんが今回は最新versionの0.17.5を使用します。

そうしたら以下のコマンドで雛形ファイルを作成します。

go run github.com/99designs/gqlgen init

いとも簡単にGraphQLサーバーが作成されました!ディレクトリ構成が以下のようになっていれば正常です。

生成されたファイルの説明

  • graph/generated/generated.go
    このファイルはGraphQLサーバーに対するリクエストを解釈しgraph/resolver.goの適切なメソッド呼ぶ役割を果たしています。
  • graph/model/models_gen.go
    schemaで定義したtypeやinputをgolangの構造体に変換したものが定義されます。
  • graph/schema.resolver.go
    リクエストを元に実際の処理を実装するresolverファイルです。

上記の3つはschemaを変更した後go run github.com/99designs/gqlgen generateを実行することでコードが再生成されます。

  • graph/resolver.go
    ルートとなるresolver構造体が宣言されます。再生成はされません。
  • graph/schema.graphqls
    GraphQLスキーマを定義するファイルです。このファイルをもとに他のファイルが再生成されます。
  • gqlgen.yml
    gqlgenの設定ファイルです。今回は行いませんがshcemaの分割などの設定もこのファイルで行うことができます。

早速graph/schema.graphqlsを見てみましょう。

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

身に覚えのないschemeが生成されています。gqlgenがデフォルトで生成してくれるものですね。今回はこのschemaをベースに簡単なtodoアプリを作っていこいうと思います。
冒頭でも記載しましたがgqlgenはスキーマファーストでGraphQLサーバーを生成してくれます。コードを自動生成する際やresolver(後述)を実装していく際もこのスキーマベースに実装していくことになります。何か意図しないエラーが出た際はこのschemaから確認していくと良いと思います。

次にgraph/model/models_gen.goを見てみましょう。

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package model

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

先ほど見たschema内で定義ているQueryやMutation以外の型が構造体として定義されています。このファイルの構造体を使用しながら後述のresolverを実装していきます。

最後にgraph/schema.resolver.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"
	"gqlgen_tutorial/graph/generated"
	"gqlgen_tutorial/graph/model"
)

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

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	panic(fmt.Errorf("not implemented"))
}

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

何やら怪しげなメソッドがいくつか生成されています。注目すべきはCreateTodoTodosです。こちらはgraph/schema.graphqlsで定義した

type Query {
  todos: [Todo!]!
}
...省略
type Mutation {
  createTodo(input: NewTodo!): Todo!
}

こちらにそれぞれ対応しています。GraphQLサーバーに対するリクエストがgqlgenが自動生成した/generated/generated.go内でよしなに処理された後、これらのメソッドが呼び出されます。そのためこれらのメソッドはいわゆるControllerの役割を果たしているわけです。
今の段階だと中身が空で何も実装されていないのでこれから中身を実装していきます。データはデータベースに永続化されることが多いですが、とりあえず動作確認のためインメモリ上にデータを保管します。

まずはgraph/resolver.goを変更します。

type Resolver struct {
	todos []*model.Todo // 追加
}

schema.resolver.goで定義されているmutationResolverやqueryResolverはこのResolverをラップしているのでまずはこのResolverにtodosを保持するようにしたいと思います。

次にresolverを実装していきます。

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
}

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	return r.todos, nil
}

CreateTodoメソッドはリクエストされた情報(input model.NewTodo)からTodoモデルを作成しResolver struct内のtodos sliceに追加するだけの処理です。

Todosメソッドでは既存のResolver struct内のtodos sliceを返しているだけです。

実際のアプリケーションであればもっと複雑なロジックやDB操作が入るかと思いますが今回はできるだけ最小限でいきたいので割愛します。

resolverの実装が終わったので、いざ動作確認!GraphQLサーバーを立ち上げます。

go run server.go

http://localhost:8080/にアクセスしましょう。

このようなページが表示されたら成功です!GraphQL playgroundを使うことでフロントエンドを用意したりわざわざcurlコマンドを叩くことなく動作確認できます。
早速mutationをリクエストしてtodoを作成してみましょう!以下のリクエストをコピペした後、画面左上の再生マーク?を押してリクエストしてみて下さい。

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

以下のようなレスポンスが返ってくれば正常に作成されています!

{
  "data": {
    "createTodo": {
      "user": {
        "id": "1"
      },
      "text": "todo",
      "done": false
    }
  }
}

次に作成したtodoを取得してみましょう。

query {
  todos {
    text
    done
    user {
      name
    }
  }
}
{
  "data": {
    "todos": [
      {
        "text": "todo",
        "done": false,
        "user": {
          "name": "user 1"
        }
      }
    ]
  }
}

素晴らしい!
resolverを実装するだけで、こんなにも簡単にGraphQLサーバーができました。
しかしこれだけだとGraphQLのメリットの一つである取得するデータの取捨選択ができていません。
例えば今の段階ではtodosを取得する際、必ずuserも取得されます。もちろん

query {
  todos {
    text
    done
  }
}

のようなリクエストを送ればレスポンスとしてuserデータが返ってくることはありませんが、内部的にはuserデータを取得しています。よくあるRDBを使用したwebアプリケーションではtodoモデルはUserIDだけを持っていてそれに紐づくuserはtodoとは別にDBから取得する場合が多いと思います。その場合、レスポンスとしてuserデータが不要なリクエストでも不要なsqlが走ってしまう訳です。
このままだとGraphQLの恩恵を十分受けていると言えません。これはtodoResolverを実装することで解決できます。ここから修正していきましょう。

まず必要なことは新しくモデルとしてTodo structを作成することです。今の状態ではschema.graphqlsを元にmodels/models_gen.goに自動生成したTodoモデルが使用されます。しかしこれだとmutaionであるCreateTodoの返り値であるTodoモデルにUserを必ず含める必要が出てきてしまうため新しくTodoモデルを定義します。

新たにgraph/models/todo.goを以下の内容で作成して下さい。

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

そしてgqlgen.ymlファイルに以下を追加します。

models:
  Todo:
    model: gqlgen_tutorial/graph/model.Todo

これによって「todoモデルはgqlgen_tutorial/graph/model.goのTodo structを使う」という指定ができる訳ですね。

準備が整ったのでファイルを再生成しましょう。

go run github.com/99designs/gqlgen generate

graph/model/models_gen.goを見て下さい。Todo structが消えていますね。これで先ほど定義したTodoモデルが使用されるようになりました。

そしたらresolverを実装していきましょう。schema.resolvers.goを開いて下さい。
Todo structは先ほどの変更によってUserではなくUserIDを保持するようになったので
まずCreateTodoを修正します。

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()),
		UserID: input.UserID, // 修正
	}
	r.todos = append(r.todos, todo)
	return todo, nil
}

次に先ほどまでなかったUserメソッドに

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
	return &model.User{ID: obj.UserID, Name: "user " + obj.UserID}, nil //修正
}

上記の変更を加えます。

ここが自分が最初なかなか理解できない点だったのですが、
mutationのCreateTodoやqueryのTodosによってtodo structが返された際、schema中のtodo typeが指定しているUserフィールドをtodo structが持っていないためtodoResolverに対して定義されているメソッドであるUserメソッドが呼び出される訳です。

これで完成です。最後にもう一度

go run server.go

を実行しGraphQL Playgroundで動作確認してみて下さい。先程と同じ挙動をしていれば完璧です。

最後に

いかがでしたでしょうか。わかりにくい部分多々あったかと思いますが、お役に立てれば光栄です。
マサカリや文句、不満等心よりお持ちしております!

Discussion