Go × GraphQLをはじめからていねいに
後輩メンバー向け。Go × 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」
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の基本はシンプルで、次の繰り返しです。
- スキーマを編集する(フィールド追加、クエリ追加、Mutation追加など)
-
go run github.com/99designs/gqlgen generate
を実行する - 生成されたリゾルバに処理を書く
何はともあれGo × GraphQLをやってみよう
Experience is the best teacher です。
最小限のプロジェクトを作って、gqlgenを使いながらGo × GraphQLを簡単にやってみましょう。
僕が試したときのソースコードも載せておきます。
※ 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アプリを題材にしたシンプルな例です。
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コードは以下のようになっています。
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"`
}
スキーマで定義したTodo
やUser
が、そのままGoの構造体として生成されているのが分かります。このマッピングのおかげで、GraphQLで受け渡すデータが型安全にGoで扱えるようになっています。
スキーマを修正する
ではでは、スキーマを少し拡張して「ユーザーを直接取得する」クエリを追加してみましょう。
type Query {
todos: [Todo!]!
+ user(id: ID!): User!
}
コード生成の再実行
スキーマを更新したら、再度コードを生成します。
go run github.com/99designs/gqlgen generate
すると、graph/model/models_gen.go
とgraph/schema.resolvers.go
に必要な型やリゾルバの雛形が追加されます。
リゾルバの実装
次はリゾルバの実装です。今回はお試しなので、データソースとしてDBや外部APIにはつながず、固定値をリゾルバに直接埋め込んで返す実装にします。
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
}
}
やったね!ちゃんと取得できました。
ここから拡張していけばもっといろんなことできますが、入門の入門として今回はここまでで。
-
「スキーマ」という言葉はGraphQLだけの用語ではありません。哲学や心理学、データベース設計など、さまざまな分野で「枠組み」や「設計図」を指す言葉として使われてきました。似た言葉に「スキーム」がありますが、スキームが完成度の高い具体的な計画を意味するのに対し、スキーマはその手前の“概念的・抽象的な設計”を指すことが多いです。 ↩︎
-
graph/generated.go のファイル先頭には
DO NOT EDIT.
と書かれています。ここは gqlgen が自動生成する内部処理 がまとまっている場所なので、自分で編集する必要はありません。むしろ触ると生成のたびに上書きされてしまうので、基本は「見ない・書かない」で大丈夫です。 ↩︎
Discussion