Gemcook Tech Blog
🚶‍♀️

Go × GraphQLをはじめからていねいに

に公開

後輩メンバー向け。Go × GraphQLを使って開発する流れを理解してもらうために、「この記事を読んでね」と渡せたらいいなと思って書いた記事です。

「はじめからていねいに」というタイトルは、昔お世話になった東進の参考書から借用しました

この記事のゴール

  • 以下の本に入門できるくらいになる

https://zenn.dev/hsaki/books/golang-graphql

この記事で扱わないこと

  • GraphQLの設計思想やスキーマ設計のベストプラクティス
  • N+1問題やdataloaderによる最適化
  • DB連携、外部API呼び出し
  • 認証・認可(Authentication / Authorization)
  • サブスクリプション(リアルタイム通信)
  • エラーハンドリングやバリデーション

GraphQLとは

GraphQLは、Facebook(現Meta)の提供するAPIのクエリ言語およびクエリを実行するサーバーサイドのランタイムです。RESTとよく比較されますが、大きな違いは「クライアントが欲しいデータを欲しい形でリクエストできる」ところにあります。

RESTだとエンドポイントごとに返すデータが決まっているので、欲しい情報が多すぎたり足りなかったりすることがあります(いわゆる over-fetch / under-fetch 問題)。GraphQLはスキーマを基盤にして、1つのエンドポイントから柔軟に必要なデータだけ取れるのが特徴です。

Go × GraphQLの全体像

GraphQLはざっくり4つの登場人物で構成されます。

  • クエリ
  • スキーマ
  • リゾルバ
  • データソース

Goで実装する場合は、上記に加えてGoの構造体が必要になります。

この全体像は空(そら)でも言えるようになってください。ここがぐちゃぐちゃだと、何を何のためにやっているのかが分からなくなります。

スキーマは何のためにあるの

GraphQLのスキーマ[1]は、APIで「どんなフィールドがあり、それぞれがどんな型を持つのか」を定義したものです。クライアントとサーバーが共通して参照する情報になります。

たとえば次のようにUser型をスキーマに定義すると、

type User {
  id: ID!
  name: String!
  age: Int!
}

Userにはid(ID型)、name(文字列型)、age(数値型)がある」というルールが決まります。

Goの構造体は何のためにあるの

スキーマは「どういう項目があるか」を宣言するだけで、プログラムの中で直接値を扱えるわけではありません。Goは静的型付き言語なので、実際の処理でデータを安全に扱うためには、明確な型をプログラム内に持つ必要があります。そのため、スキーマに対応するGoの構造体を定義して、値を保持できるようにします。

たとえば先ほどのUser型は、Goではこのように表現されます。

type User struct {
  ID   string
  Name string
  Age  int
}

structは構造体を意味します。この構造体があることで、コンパイル時に「文字列に数値を入れてしまった」などのミスを防ぎ、型安全にデータを扱えるようになるわけです。

リゾルバは何のためにあるの

スキーマや構造体には、実際にデータを取得して返す処理は含まれていません。たとえば「ユーザー一覧を返す」とスキーマに書かれていても、DBから取得するのか、外部APIを呼ぶのか、あるいは固定データを返すのかは決まっていません。

その中身を実装するのがリゾルバです。クエリで要求されたフィールドを“どう解決するか”を担当します。

※ リゾルバは英語の resolve(解決する)が由来で、「クエリで要求されたフィールドを“解決する=値を用意する”」という意味合いがあります。

Go × GraphQLをいい感じにできるのが「gqlgen」

https://github.com/99designs/gqlgen

gqlgenは、GoでGraphQLを扱いやすくするためのライブラリです。GraphQL自体は「欲しいデータを欲しい形で取れる仕組み」ですが、Goでそれを実装しようとすると、スキーマの定義や型の変換、リゾルバの準備など、いろいろ手間がかかります。

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スキーマを定義するだけで自動的に型安全なGoコードを生成してくれるよ、つまりスキーマファーストだぜ、ってことです。

gqlgenの使い方

gqlgenの基本はシンプルで、次の繰り返しです。

  1. スキーマを編集する(フィールド追加、クエリ追加、Mutation追加など)
  2. go run github.com/99designs/gqlgen generate を実行する
  3. 生成されたリゾルバに処理を書く

何はともあれGo × GraphQLをやってみよう

Experience is the best teacher です。

最小限のプロジェクトを作って、gqlgenを使いながらGo × GraphQLを簡単にやってみましょう。
僕が試したときのソースコードも載せておきます。

https://github.com/muranakaaa/go-gqlgen-sample

※ Goのバージョンは 1.24.3 を使用しています。

プロジェクトを作る

mkdir go-gqlgen-sample
cd go-gqlgen-sample
go mod init example.com/go-gqlgen-sample

gqlgenをインストールして初期化

gqlgenは公式でCLIツールを提供してくれており、これでプロジェクトを初期化できます。

go get github.com/99designs/gqlgen
go run github.com/99designs/gqlgen init

実行すると、以下のようなファイル群が生成されます。

  • graph/schema.graphqls : GraphQLスキーマ定義
  • graph/generated.go[2] : gqlgenが生成する内部処理コード
  • graph/model/ : スキーマから生成されるGoの型定義
  • graph/resolver.go : リゾルバの雛形
  • graph/schema.resolvers.go : スキーマごとのリゾルバ実装用の雛形
  • server.go : エントリーポイント

この時点で「GraphQLサーバーの雛形」が完成しています。楽ちんですね。

サーバーを起動する

試しにサーバーを起動してみます。

go run server.go

ログが流れて、http://localhost:8080/にて、以下のようなクエリ実行のためのGraphQL Playgroundにアクセスすることができます。

左側の入力欄に実行したいクエリを入力して、▷の実行ボタンを押すことで右側に結果が表示される仕組みです。

スキーマを確認する

graph/schema.graphqlsには、初期状態でサンプルのスキーマが生成されていました。Todoアプリを題材にしたシンプルな例です。

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

このスキーマに基づいて、自動生成されたGoコードは以下のようになっています。

graph/model/models_gen.go
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"`
}

type NewTodo struct {
	Text   string `json:"text"`
	UserID string `json:"userId"`
}

スキーマで定義したTodoUserが、そのままGoの構造体として生成されているのが分かります。このマッピングのおかげで、GraphQLで受け渡すデータが型安全にGoで扱えるようになっています。

スキーマを修正する

ではでは、スキーマを少し拡張して「ユーザーを直接取得する」クエリを追加してみましょう。

graph/schema.graphqls
type Query {
  todos: [Todo!]!
+ user(id: ID!): User!
}

コード生成の再実行

スキーマを更新したら、再度コードを生成します。

go run github.com/99designs/gqlgen generate

すると、graph/model/models_gen.gograph/schema.resolvers.goに必要な型やリゾルバの雛形が追加されます。

リゾルバの実装

次はリゾルバの実装です。今回はお試しなので、データソースとしてDBや外部APIにはつながず、固定値をリゾルバに直接埋め込んで返す実装にします。

graph/model/models_gen.go
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
	return &model.User{
		ID:   id,
		Name: "Test User",
	}, nil
}

クエリを実行

一度サーバーを落として再起動します。Playgroundから以下のクエリを実行します。

query {
  user(id: "123") {
    id
    name
  }
}

やったね!ちゃんと取得できました。

ここから拡張していけばもっといろんなことできますが、入門の入門として今回はここまでで。

脚注
  1. 「スキーマ」という言葉はGraphQLだけの用語ではありません。哲学や心理学、データベース設計など、さまざまな分野で「枠組み」や「設計図」を指す言葉として使われてきました。似た言葉に「スキーム」がありますが、スキームが完成度の高い具体的な計画を意味するのに対し、スキーマはその手前の“概念的・抽象的な設計”を指すことが多いです。 ↩︎

  2. graph/generated.go のファイル先頭にはDO NOT EDIT.と書かれています。ここは gqlgen が自動生成する内部処理 がまとまっている場所なので、自分で編集する必要はありません。むしろ触ると生成のたびに上書きされてしまうので、基本は「見ない・書かない」で大丈夫です。 ↩︎

Gemcook Tech Blog
Gemcook Tech Blog

Discussion