長らく REST API を活用してきたため、実務で GraphQL の経験がありませんでした。 特に困ったのが Query をどの単位で定義すれば良いのか?という点です。簡単そうな問題に感じますが、執筆前の自分では言語化するのが難しい状態でした。
色々と GraphQL について調べた結果、自分なりに納得できて方針が見えてきたので、備忘録としてこの記事を書いています。
リソースを元に考える
REST API では/usersや/postsのようにリソース単位でエンドポイントを定義します。
GraphQL でも同じように Query を定義しますが、リソースを元に考えるという観点では REST API と GraphQL に大きな違いはありません。
※ ここでいうリソースというのは、実際にはドメインモデルや DB のレコードに対応するオブジェクトなど
type Query { users: [User!]! } query GetUsers { users { id name } }
この時点では、REST API と比較して GraphQL では取得するデータをフィールド単位で取捨選択できる(データ取得のオーバーヘッドを削減できる)という点での優位性しかありません。 自分の考えでは、膨大な数のリクエストが来るような大規模なサービスを除き、あえて GraphQL を採用する大きな理由にはなりません。
ユースケースはクライアントが定義する
GraphQL の強みはqueryを組み合わせて、さまざまなユースケースをクライアントで自由に定義できる点です。
GraphQL では/graphqlという単一のエンドポイントでリクエストを受け付けるため、リクエスト(.query)の書き方を変更するだけで、直接には関連をもたないリソースであっても一度のリクエストで取得することができます。
こういったリソースを組み合わせれば解決するケースでは、専用のQueryをわざわざ定義するのは避けるべきです。
type Query { users: [User!]! settings: [Setting!]! } query GetUsersAndSettings { users { id name age } settings { id contents { enabled label } } }
REST API の場合、シンプルさは魅力ですが、思いつくアプローチはどれも微妙です。
- 新たなエンドポイントを定義する: ユースケースが増える度に対応が必要
- クエリ文字列で指定する: APIの複雑化
/usersのレスポンスにsettingsを含める: データのオーバーフェッチbffを使う: アーキテクチャの複雑化- backend for frontend: データ集約を行うレイヤーを挟むアーキテクチャ
GraphQL はこの問題を見事に解決しています。
新たにクライアントで定義したリクエスト(.query)は不要になれば、捨てれば良いです。フィールドに対する Resolver などの拡張は必要ですが、ユースケースの拡張に合わせて必ずしもサーバー側の変更は必要になりません。
一方、GraphQL のトレードオフにはキャッシュ・認証の複雑化、思わぬ負荷(N+1が発生しやすい)がよく挙げられるため、注意が必要です。
複雑なユースケースはどう定義する?
前提 GraphQLはCQRS
GraphQL はデータの取得(Query)と変更(Mutation)が分離されており、いわゆる CQRS(Command Query Responsibility Segregation)のパターンに該当します。ユースケースに合わせてデータの取得方法をクライアントが定義できる点はまさに CQRS です。
しかし、backend で CQRS を扱い際に利用することが多いQuery Serviceほどの柔軟性さはなく、実際にどのような経路でデータが取得されるかは知りません(データとの物理的な距離による制約)。
リソースの組み合わせで解決できないなら、専用Queryを作るしかない
クライアントのリクエストだけで完結できない複雑なユースケースの場合、どうすれば良いでしょうか。 よくあるのは、月間のレポートを表示するといった集計処理が必要になるようなケースです。クライアント側で関連するデータなどを全件、取得して、集計処理を行うのはリソース効率の観点で考えると悪手といえます。
不必要に集計ロジックがフロントに露出してしまう点も微妙...
const query = gql` query GetUserActivities { users { id activity: { kind timestamp } } } `; const res = client.request(query); const aggregated = res.data.users.reduce((user, accum) => { ... })
複雑なユースケースの解決策として、リソースの組み合わせで表現できない・しにくい場合は専用の Query を定義することが考えられます。 重要なのはリソースの組み合わせでは表現できない・しにくい場合のみということ。基本的にはリソースを元にクライアント主導で考えるべきで、新たな Query として切り出すのは最終手段です。 次々に専用 Query として切り出してしまうと、GraphQL の強みを生かせず管理コストが爆増します。
元となるリソースが存在しないユースケース
先ほどの複雑なユースケース(集計処理)は別の捉え方ができます。
集計結果というのはドメインモデルとして定義されているわけでもない、あるデータから変換して作られるデータです。つまり、ある Query の結果として表現されるデータであり、元となるリソースが存在しないため、別のQueryとして切り出せば良いと考えられます。
まとめ
- 常にリソースを元に考える
- クライアントでリソースを組み合わせて、さまざまなユースケースを定義する
- 複雑なユースケースの場合、専用のQueryを定義することを検討する

