gqlgenのチュートリアルを試しました


はじめに

初めまして、k.s.ロジャースの西谷です。

直近では、iOS, Android用のREST APIを開発しております。
このときに、画面描画用データ取得APIが350kb程のレスポンスを返すことがあり、結果としてアプリの描画速度に影響を与えました。
原因はレスポンスのネストしたEntityサイズで、運用当初はデータ量が少ないため問題はありませんでしたが、
運用しているうちにデータが増加することでEntityサイズが膨れ上がりました。

このときの対応としては、レスポンスから不要な内容を削除し、350kbから20kbまで減らすことで正常化できました。
しかし、今回は偶然不要データでしたが本来は返すべき内容ですので、方針として特例を多く作りたくない気持ちがありました。

レスポンスサイズ レスポンス速度 アプリの画面表示までの時間
全データを返す 350kb 9.5秒 15~20秒
必要データだけ返す 20kb 2.5秒 5秒

そこで、各画面・機能にとって必要なデータだけを柔軟に取得できるGraphQLについて調査しました。
本来GraphQLで処理速度が速くなるわけではありませんが、上記の場合は不要Entityを取得しないため、サーバ側の処理速度改善が期待できると考えています。

間違い・助言等があればコメントにてお知らせいただけたらと思います。

なぜgqlgenなのか

まず、Golangで開発したい理由があります。
弊社ではGolangのボイラーテンプレート(REST, gRPC)を開発しており、GraphQLを採用するとなった場合はGolangで実装する可能性が高いです。

golangで実装可能なフレームワークはこちらを参考にさせて頂きました。
弊社ではiOSやAndroidチームと連携して開発を進めることが多いため、スキーマ駆動かつドキュメントが豊富なgqlgenを選択しました。

環境構築

公式のチュートリアルに従って進めます。

まずプロジェクトの作成を行います。

mkdir gqlgen-todos
cd gqlgen-todos
go mod init github.com/[username]/gqlgen-todos

スキーマファイルを作成します。

schema.graphql
# Todoのデータ保持
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

# Userのデータ保持
type User {
  id: ID!
  name: String!
}

# リクエストの定義(Todo作成)
input NewTodo {
  text: String!
  userId: String!
}

# データ取得APIの定義
type Query {
  todos: [Todo!]!
}

# データ変更APIの定義
type Mutation {
  createTodo(input: NewTodo!): Todo!
}

データ取得関連のAPIはQueryに、新規作成・更新・削除はMutationに定義することになります。

プロジェクトの雛形作成

スキーマファイル作成後にgo run github.com/99designs/gqlgen initで雛形を生成できます。

.
├── generated.go # gqlgenの生成ファイル(基本的に触らない)
├── go.mod 
├── go.sum
├── gqlgen.yml # 設定ファイル
├── models_gen.go # スキーマから生成されたモデル
├── resolver.go # 処理の実装をここに書く
├── schema.graphql
└── server
    └── server.go

サーバ側の実装

Todoモデルの定義

Todoのモデルを新規定義します。
models_gen.goで自動生成されたモデルはUserも含まれています。
しかし、リクエストでユーザデータを指定されたときのみ、データ取得するようにしたいためtodoを再定義しています。

todo.go
package gqlgen_todos

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

gqlgen.ymlに新規作成したtodoの内容を追加します。

gqlgen.yml
schema:
- schema.graphql
exec:
  filename: generated.go
model:
  filename: models_gen.go
resolver:
  filename: resolver.go
  type: Resolver
# 追加
models:
  Todo:
    model: github.com/[username]/gqlgen-todos.Todo

再ビルドします。

go run github.com/99designs/gqlgen

resolversの実装

現在の仕様でgqlgenを実行しても、すでに存在するresolver.goを変更出来ないようです。(対応予定)
ですので、一度resolver.go削除して再生成します。

rm resolver.go
go run github.com/99designs/gqlgen

これでresolver.goが最新になり、not implementedのメソッドを実装して完了です!

resolver.go
package gqlgen_todos

import (
    context "context"
    "fmt"
    "math/rand"
)
// 追加
type Resolver struct {
    todos []*Todo
}

func (r *Resolver) Mutation() MutationResolver {
    return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
    return &queryResolver{r}
}
func (r *Resolver) Todo() TodoResolver {
    return &todoResolver{r}
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    // 追加
    todo := &Todo{
        Text:   input.Text,
        ID:     fmt.Sprintf("T%d", rand.Int()),
        UserID: input.UserID,
    }
    r.todos = append(r.todos, todo)
    return todo, nil
}

type queryResolver struct{ *Resolver }

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

type todoResolver struct{ *Resolver }

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

実行

go run server/server.goでモックサーバが起動してhttp://localhost:8080/からテストできます。

後は次のようにTodo追加と一覧取得ができます。

おわりに

今回はgqlgen公式のチュートリアルを試してみました。
現在はシンプルですが、実用にあたっては対応すべき課題が多々あると思います。
今後も時間を見つけて調査を進めたいと思います。

Wantedlyでもブログ投稿してます

Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers