GraphQL
https://gyazo.com/22cf18a8cf7107085898c8562a7269e6
公式ドキュメント: https://graphql.org/
仕様書: https://spec.graphql.org/
エコシステム: https://graphql.org/community/tools-and-libraries/
概要
Meta によって開発された Web API のための クエリ言語
特殊な 通信プロトコル を必要とするものではない
gRPC とは大きく異なる点
REST API と比較して、API の速度や柔軟性、開発のしやすさを重要視して設計されている
GraphQL Schema と呼ばれるスキーマファイルを、独自のスキーマ定義言語(GraphQL SDL)で定義し、独自のクエリ言語でリクエストを行う
これにより、特定の言語やフレームワークに依存しない
一方、学習コストは別途発生する
独自のクエリ言語でリクエスト
Query: データ問い合わせ
Mutation: データの書き換え
Subscription: データの購読
特徴
REST API の問題が解消されている
REST API#66de436675d04f0000eba145
単一の API エンドポイント
独自のクエリ言語を利用してリクエストを行う
クライアントが必要してデータを返す
Over-fetching や Under-fetching を回避可能
モバイル端末の普及によりニーズが増えた
限られた帯域幅で効率的にデータを取得する必要性が増した
Web と iOS、Android 間で異なる UI の要求に答えられるようにするため
UI コンポーネントによる自律分散したデータ取得が可能
REST API で UI コンポーネントを構築する場合、大きく以下の 2 つの選択肢が取れる
1. ページコンポーネントで必要なデータを取得する
親コンポーネントが子孫コンポーネントの必要なデータまで管理する必要があり、凝集度 が低い(低凝集)
2. 各コンポーネントが対応するデータを取得する
各コンポーネントでリクエストが発生するため、パフォーマンスに懸念がある
GraphQL の場合、Fragment という機能で回避する
GraphQL#66f1297575d04f00000dcd97
Fragment を用いて、各 UI コンポーネントに必要なデータを宣言することで自律分散したデータ管理が可能に
Fragment Colocation と呼ばれる設計手法
各コンポーネントで宣言された Framgment は最終的に 1 つのクエリにまとめられてリクエストされるため、パフォーマンスの懸念も解消される
強力な型システムとツールチェーン
ビルドインの強力な型システムを提供
code:graphql
task Task {
id: Int!
name: String!
dueDate: String!
status: String!
description: String
}
加えて、以下のような強制力がある
1. GraphQL Schema を定義するのが必須
ただし、Code first というアプローチの場合は不要 radish-miyazaki.icon
2. リクエストやレスポンスに型違反がある場合は、ランタイムエラーとなる
この強制力により、以下のメリットが享受できる
GraphQL Schema を API ドキュメントとして活用できる
GraphQL Schema を定義するのが必須であるため、API ドキュメントのない状態が生まれない
型違反をランタイムエラーとするため、ドキュメントと実装の乖離が起きづらい
イントロスペクションという機能も提供する
https://graphql.org/learn/introspection/
GraphQL サーバにて対してクエリを投げることで、GraphQL Schema に関するデータを取得できる機能
これを活用したのが GraphiQL
GraphiQL を用いると、GraphQL Schema に基づいた API ドキュメントを参照できる
また、GraphQL Schema に基づいた補完も効く
潤沢なサードパーティ製のコード生成ライブラリも存在する
TypeScript :GraphQL-Codegen
API を進化させやすい
deprecated ディレクティブ
非推奨を示すためのアノテーションが組み込みで用意されている
code:gql
type Post {
publishedAtUnixTime: Int! @deprecated(reason: "代わりに publishedAt を使用してください")
publishedAt: String!
}
GraphQL Inspector を用いると、非推奨の使用を検知することができる
ユースケースがフィールドレベルで確認できる
REST API : クライアントがレスポンスのどのフィールドを実際に使用しているのかは、実装を見るまで分からない
GraphQL : クエリを見れば分かる
不要なフィールドを削除する 意思決定 に役立つ
問題点
REST API と比較すると情報が少なく、学習コストがかかる可能性がある
クライアントのリクエストに応じて異なるので、サーバ側の負荷予測が難しい
クライアントが自由にクエリを作成できるため、大量のフィールドや深いネストのクエリが発行されると、サーバに負荷がかる
負荷への対策が必須
クエリの負荷監視
Datadog APM による モニタリング
Sentry によるエラートラッキング
(外部向け)高負荷なクエリを未然に防ぐために、クエリのフィールド数やノード数、深さに制限を加える
Depth Limiting
Query Cost Analysis
(内部向け)事前に登録されたクエリのみを受け付けるようにする
Persisted Query
Signed Query
キャッシュが複雑になりやすい
REST API の場合、エンドポイントごとにキャッシュすればよいので管理が楽
N + 1 問題 が発生しやすい
GraphQL における N+1 問題の発生しやすさはデメリットなのか
REST API との比較
table:_
REST API GraphQL
エンドポイント リソースごと 1 つ
データ操作の種類 GET / POST / PUT / DELTETE Query / Mutation
データ取得の柔軟性 低い 高い
型 弱い 強い
学習難易度 低い 高い
Fragment
https://graphql.org/learn/queries/#fragments
クエリを再利用可能にする機能
重複した箇所を Fragment として切り出すことで、DRY なクエリを作成可能
定義には fragment キーワードを用いて、任意の名前を付ける必要がある
また、どの型に属する Fragment なのかを示すために、on {型名} を記述する必要がある
code:gql
fragment PostItem on Post {
title
author {
name
posts {
tags {
name
}
}
}
}
利用側
code:gql
query DryQuery {
post1: post(id: "1") {
...PostItem
},
post2: post(id: "1") {
...PostItem
},
}
変数
https://graphql.org/learn/queries/#variables
https://spec.graphql.org/draft/#sec-Language.Variables
各クエリは、変数を用いて外から値を受け取るようにすることが可能
変数
$ から始まる任意の変数名
変数毎に型を指定する必要がある
変数として利用する値は、クエリとは別に作成する必要がある
GraphiQL
ページ下部の Variables から JSON 形式で入力する
https://scrapbox.io/files/66f12afa45646c001db5b425.png
ディレクティブ
@ で始まる修飾子
GraphQL Schema とクエリの両方で付与できるが、種類によってはどちらか一方でしか使えないものもある
ビルドインのものもあれば、独自で定義することも可能
https://spec.graphql.org/draft/#sec-Type-System.Directives.Custom-Directives
code:gql
directive @constraint(maxLength: Int) on FIELD_DEFINITION
GraphQL Schema
@deprecated: フィールドが廃止予定であることを知らせるビルドインディレクティブ
https://spec.graphql.org/draft/#sec--deprecated
e.g.
code:gql
type Post {
title: String! @deprecated
body: String! @deprecated(reason: "Use content instead.")
content: String! @constraint(maxLength: 1000)
}
クエリ: https://graphql.org/learn/queries/#directives
@include: if に渡された値が true の場合のみ結果を取得する
https://spec.graphql.org/draft/#sec--include
@skip: if に渡された値が true の場合のみ結果を取得しない
https://spec.graphql.org/draft/#sec--skip
これらは、計算負荷が非常に高いフィールドをある条件でしか利用しない場合や、ユーザが特定の権限を持つ場合のみ取得したい状況で有用
warning.icon ただし、クエリの 認知的負荷 を上げたり、最終的な実行結果が分かりづらくなるため、困った時に使う手段として捉えておくのが良い
e.g.
code:gql
query IncludeQuery($showRanking: Boolean!) {
post {
title
ranking @include(if: $showRanking)
}
}
参考
Software Design 2024年9月号
Web APIの設計
Goで学ぶGraphQLサーバーサイド入門
NestJS × Reactで学ぶフルスタックGraphQLアプリケーション開発