はじめに
こんにちは、medibaバックエンドエンジニアの@tmkinoueです。
本記事は「mediba Advent Calendar 2023」6日目の記事です。
今回は今年担当したCMSの開発案件で初めてGraphQLを使ったAPI実装を行ったので感想を書こうと思います。※あくまで個人的な感想です。
やったこと
環境
- go 1.18
- github.com/99designs/gqlgen v0.17.31
今回開発したCMSでの機能を例に見ていきたいと思います。ここではユーザーに提供するプレゼントを管理する機能を例に上げます。
CMS上でプレゼント一覧ページに提供する機能と、各プレゼントの詳細ページに提供する機能を求められています。これを以下のようなスキーマ設定で一つのGraphQLのqueryリクエストで処理しました。
type Query {
"""
プレゼント一覧
"""
presents(
presentsCondition: PresentsCondition
): Presents!
}
"""
プレゼント一覧
"""
type Presents {
presents: [Present!]!
}
"""
プレゼント
"""
type Present {
"""プレゼントID"""
id: Int!
"""タイトル"""
title: String
"""公開期間開始日時"""
publicStartDate: String!
"""公開期間終了日時"""
publicEndDate: String!
"""プレゼント在庫情報
stock: Stock
}
"""
プレゼント在庫情報
"""
type Stock {
"""総数(総在庫数+未入庫在庫数)"""
all: Int!
"""総在庫数"""
total: Int!
"""現在庫数"""
current: Int!
}
"""
プレゼント検索条件
"""
input PresentsCondition {
"""ID指定検索"""
idFilter: Int
"""タイトル検索条件"""
titleFilter: String
}
もともとタイトル検索の要望があったのもあり、フィルタリング機能にID指定を追加し、詳細ページへのレスポンスに対応させました。
一方、在庫情報は詳細ページでだけ確認できれば良いという要件で、プレゼントのマスタ情報と異なり、集計してレスポンスを返す必要があるため一覧ページで全プレゼントの在庫を取得して返却すると処理コストが高くなる懸念がありました。
そこで明示的なリゾルバーの使用の設定をすることで在庫情報の取得をリクエストされたときだけ集計して返却するようにしました。
やり方としてはgqlgen.ymlに以下のような設定をします。
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
Present:
fields:
stock:
resolver: true
この設定を追加してコード生成することでresolverが分割されます。そしてtypes.resolver.goのstock関数に在庫集計の処理を書いていくことで、在庫集計をリクエストされたときだけ処理を実行するように設定できました。
package resolver
// 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.
// Code generated by github.com/99designs/gqlgen version v0.17.31
import (
"context"
"fmt"
"wellness-backend/graph/gqlserver"
"wellness-backend/graph/model"
)
// Presents is the resolver for the presents field.
func (r *queryResolver) Presents(ctx context.Context, presentsCondition *model.PresentsCondition) (*model.Presents, error) {
panic(fmt.Errorf("not implemented: Presents - presents"))
}
// Query returns gqlserver.QueryResolver implementation.
func (r *Resolver) Query() gqlserver.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
package resolver
// 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.
// Code generated by github.com/99designs/gqlgen version v0.17.31
import (
"context"
"fmt"
"wellness-backend/graph/gqlserver"
"wellness-backend/graph/model"
)
// Stock is the resolver for the stock field.
func (r *presentResolver) Stock(ctx context.Context, obj *model.Present) (*model.Stock, error) {
panic(fmt.Errorf("not implemented: Stock - stock"))
}
// Present returns gqlserver.PresentResolver implementation.
func (r *Resolver) Present() gqlserver.PresentResolver { return &presentResolver{r} }
type presentResolver struct{ *Resolver }
感想
良かったこと
GraphQLではクライアントから必要な項目をリクエストしてもらい、処理分割することで一画面、一機能に特化してAPIを作らなくて済み、工数やコードの記述量削減に繋がったと思います。
反省点
- フロントエンド開発者との連携
良くも悪くもフロントエンド側からのデータ取得の自由度が上がるので連携がうまく行かず、結合試験の段階で認識齟齬からの初歩的なバグがあったのは反省点。
今回はスキーマファイルとイントロスペクション上で確認する形で情報連携していましたが、使い慣れていないこともあり情報連携がうまくいっていなかったと思うので改善点を考えたい
- エラー時のレスポンス
エラーレスポンスについてはerrors
内の extensions
を拡張するやり方とエラー用のスキーマを定義するやり方があるらしい。最初のスキーマ定義の時点でエラーレスポンスをちゃんと考慮していなかったので後者のやり方を検討できなかったのは反省点。。
最後に
自分は今年の4月に異動となり、このサービスを担当することになりました。
今回GraphQLを使った実装は自分が主に担当としてやらせていただきましたが、自分の異動前からGraphQLについてキャッチアップしプロトタイプの実装をしてくれていたチームメンバーがいます。そのチームメンバーからの情報共有やコードレビューなどの協力もあって開発を終えることができましたので改めてお礼申し上げたいと思います。ありがとうございました。