はじめに
初めまして、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
スキーマファイルを作成します。
# 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を再定義しています。
package gqlgen_todos
type Todo struct {
ID string
Text string
Done bool
UserID string
}
gqlgen.ymlに新規作成したtodoの内容を追加します。
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
のメソッドを実装して完了です!
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