GraphQL
TODO:
- ロギングについて
メモ:
GraphQL の良いところ、スキーマファーストで開発すると、型のおかげで不正なクエリをクライアントサイドでサーバに送信する前に事前にエラーにすることができるところ
GraphQL とは?
データ取得/操作のためのクエリ言語、及び、任意のバックエンドに対しそのクエリを実行するためのサーバサイドのランタイムのことを指す。
Facebook が 2012 年ごろから開発しており、2015 年にリリース、OSS 化された。現在はコミュニティによりメンテナンスされている。
GitHub や Twitter で利用されている。NewRelic にも GraphQL の API が生えているようだ。
OSS として公開されているのは GraphQL の仕様としてのドキュメント。以下より参照できる。
学習方法
公式サイトがあり、基本的な事項は学ぶことができる。GraphQL の機能や型システムについて、さらに利用する場合のベストプラクティスがいくつか載っている。
GraphQL のコミュニティーにより OSS として開発されている GraphQL のチュートリアルがある。このサイト自体も、GraphQL Server である Prisma をバックエンドに構築されている。
解決しようとした課題
モバイル端末増加による効果的なデータロードの必要性
電源の心もとない端末およびクソザコ回線が、Facebook が GraphQL を開発した理由
ネットワーク上でやり取りされるデータ量を最小化する
クソザコ環境下でのパフォーマンスが向上する
様々な異なるフロントエンドフレームワークおよびプラットフォーム
様々なフロントエンドフレームワーク上で動作するクライアントに対し、それらすべての要求に応えられるような一つの API を開発するのは大変
各種クライアントは的確に必要なデータにアクセスできる
素早い開発およびリリース
REST API では、サーバ側の API 設計が、クライアント側の要件や設計変更を考慮する必要がある
これは素早い開発を邪魔する
REST と GraphQL
データのフェッチが GraphQL のが楽
Over- and Underfetching の解消
Overfetching (不要なデータの取得):
REST API は固定されたデータ構造を返すので、本来は必要ないデータがそこに含まれている場合がある
GraphQL は要求したデータのみ過不足なく返ってくる
Underfetching (必要なデータの不足):
REST API は最終的に必要なデータの取得のために、複数の API エンドポイントを叩いて回る必要がある場合がある
GraphQL は一回のクエリで必要なデータを全て要求できる
素早いプロダクトのイテレーション
API 設計を柔軟に変えられない
バックエンドでの分析
必要なデータのみを取得するので、バックエンドで解析すれば、クライアントが本当に必要としているデータがなんなのかがわかる
リゾルバのパフォーマンスを計測することで、システムのボトルネックを発見できる
スキーマ及び型システム
一般的な型システムとは異なり、明示しない限りは全ての型がデフォルトで Nullable になっている (大抵、むしろ Nullable の場合に明示する)
DB や他サービスをバックに据えていると予期しない事態に陥る場合が多いため、リクエストが完全に失敗するよりも Null で返した方が良い、という方針のようだ
Non-null も指定できるので、どのフィールドを Non-null とするのかが割と重要になる
GraphQL のアーキテクチャ
GraphQL は仕様としてのみリリースされている
GraphQL サーバーの振る舞いを詳細に解説したドキュメントに過ぎない
サーバは自分たちで実装するか、既存のものを利用する
ユースケース
データベースと接続された GraphQL サーバー
GraphQL サーバーはクエリを受け取ると、それに基づいて DB にクエリを投げなおす (クエリを 解決する)
GraphQL は トランスポート層に依存しない
どんなネットワークプロトコルでも利用可能
TCP, WebSocket ベースで GraphQL サーバーを開発しても良い
複数のレガシーなサードパーティシステムの前に配置された薄いレイヤーとしての GraphQL サーバー
データベースおよびサードパーティシステムを統合した GraphQL サーバー
リゾルバの機能
どうやって GraphQL を様々なユースケースに適用させるのか?
GraphQL では、各種フィールドが、リゾルバーと呼ばれる一つの機能に対応づいている
サーバーがクエリを受け取ると、受け取ったすべてのフィールドに対し対応する関数を呼び出し、クエリを解決する
すべての関数が結果を返してきたら、それをフォーマットして返す
GraphQL クライアントライブラリ
GraphQL はREST API での数々の欠点や不便さを解消してくれているので、フロントエンドエンジニアには良い
over- underfetching
複雑さはサーバサイドに詰め込まれている
クライアントはデータがどこからフェッチされたか気にしなくて良いし、それ単体で利用できる
REST API からデータをフェッチする場合、多くのアプリケーションは以下を行う
HTTP リクエストの構築と送信
レスポンスを受信しパースする
ローカルにデータを保存 (メモリや永続化)
UI にデータを表示
declarative data fetching (= 記述的データ取得) アプローチでは、以下で済む
データ要求を記述する
UI にデータを表示する
低レベルやネットワーキングの課題は抽象化され、データの依存関係が支配的
GraphQL クライアントライブラリである Apollo や Relay はこれを実現している
GraphQL ベストプラクティス
GraphQL 仕様は、あえて API が直面する重要な問題について言及していない
ネットワークを介しての認証、ページネーション
GraphQL を使うと解決できないわけではなく、GraphQL が 何か に対する説明から逸脱しているだけ
HTTP
REST API は、リソースごとに URL を生やしている
GraphQL は、1つのエンドポイントを生やし、そこでリクエストを受け付ける
複数のエンドポイントに分割することもできるが、GraphiQL 等のツールから利用しづらくなる
JSON (with GZIP)
GraphQL は大抵レスポンスとして JSON を返すが、仕様はそれを要求して いない
ネットワークのパフォーマンスを考えると API レイヤーで JSON を使うのは奇妙だが、JSON はテキストなので、GZIP で圧縮できる
GraphQL サーバーは GZIP を扱えるものが多く、またその際にクライアントに Accept-Encoding: gzip をヘッダーにつけるよう要求する場合が多い
Versioning
GraphQL は REST API のようにバージョニングできないわけではないが、バージョニングを避けることを強く意識している
そもそも、なぜ API にバージョンが必要なのか?
API エンドポイントから返されるデータ構造にクライアントが依存していると、いかなる変更も 破壊的な変更となり、破壊的変更には新しいバージョンが必要となる
API に機能追加するたびに新しいバージョンが必要なら、リリース間でトレードオフが生じ、数多くのバージョンが必要となり、その度にAPI を理解し、保守する必要がある
GraphQL は、明示的にリクエストされたデータのみを返す
新しい型、新しいフィールドの追加によって機能を追加でき、これらは既存の破壊的な変更なしで行える
常に破壊的な変更を回避でき、バージョンレスな API を提供できる
Nullability
大抵の型システムでは、通常型に加えて nullable な型を明示的に定義する
GraphQL の型システムでは、全てのフィールドがデフォルトで nullable になっている
バックにデータベースや他のサービスを抱えたネットワークサービスは、予期しない自体に陥ることが多いため
データベースは落ちるかもしれないし、非同期なアクションは失敗するかもしれないし、例外がスローされるかもしれない
リクエストに完全に失敗するよりも、null を返してくれた方が良い、という判断
GraphQL スキーマの設計では、何か問題が発生した場合、特定のフィールドの値が null になるのが適切かどうかを考えるのが重要
適切でなければ non-null を利用する
non-null の値がエラーとなったら、その親フィールドが null になる
Pagination
ページネーションの設計方法は数多くあり、各々にメリット/デメリットがある
Connector というデザインパターンがあるようだ
Server-side Batching & Caching
GraphQL は、サーバーサイドでコードを綺麗に描けるように設計されている
全ての型の全てのフィールドは、その値を解決するための機能という1つの目的にのみ集中すれば良い
しかし、深く考えないと GraphQL サービスは おしゃべり になる
複数回に渡り DB にデータからデータをロードしてしまう
バッチ技術 で解決できる
バックエンドからの複数のデータ要求は短期間収集され、1つのリクエストに集約して DB やマイクロサービスに送られる
GraphQL におけるクエリ
簡単に言うと、オブジェクト上の特定のフィールドに対する問い合わせ
フィールド
問い合わせと問い合わせ結果が似通っているのが特徴。問い合わせたフィールドに対し答えが返ってくる
ユーザは、どのような形で結果が返ってくるかがわかる
サーバーは、クライアントがどのフィールドについて知りたいのかがわかる
下記例では、Star Wars のヒーロとその名前、さらにその友達たちの名前をリクエストしている
hero の name は String 型で返ってくる
friends については、Object が複数返ってくる (ただし、問い合わせたフィールドについてのみ)
code:リクエスト
{
hero {
name
friends {
name
}
}
}
code:レスポンス
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
...
引数
REST API の場合
クエリパラメータ、パスパラメータによって、1つのフィールドに対して 1 組の引数群を渡せる
GraphQL の場合
フィールドを複数定義でき、各フィールドに対して各々独自に 1 組の引数群を渡せる
GraphQL では引数に様々な型を持たせることができる
下記例では、高さの単位 (unit) が FOOT か METER かで、前者のものを取得するように要求している
自身で独自に型を定義することもできる
code:リクエスト
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
code:レスポンス
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
エイリアス
レスポンスのフィールドは、リクエストスキーマ内のフィールドは含まれるが、引数は含まれない
異なる引数で同じフィールドに対しリクエストした時、識別できないのでは?
これを解決するために、エイリアス が利用できる
下記例では、hero フィールドがコンフリクトしているが、: で挟んでエイリアスを定義している
レスポンスでは、フィールド名の代わりにエイリアスが利用されている
code:リクエスト
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
code:レスポンス
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
フラグメント
同じようなクエリをなんども投げる必要がある場合がある
例) 複数の hero について、その名前とその友達達の名前を取得したい
フラグメント を利用すると、再利用可能なクエリを定義できる
code:リクエスト
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
code:レスポンス
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
インラインフラグメント
GraphQL にはインタフェースや型合成の機能がある
具象型で条件分岐したい場合は、インラインフラグメント が利用できる
普通のフラグメントも型をアタッチして利用するものなので、似たようなことはできる
下記に例を示す
hero は Character 型を返す
Character 型の具象型には Droid と Human がある
Droid の場合は primaryFunction、Human の場合は height を取得したい
code:リクエスト
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
code:データ
{
"ep": "JEDI"
}
code:レスポンス
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
オペレーション名
これまでの例ではシンタックスを一部省略していたが、記述することで曖昧さをなくせる
オペレーションタイプを query、オペレーション名を HeroName として指定すると、以下のようになる
オペレーションタイプ: query、mutation, subscription
簡略構文を使用している場合を除いて必須
使用している場合、オペレーション内でその名前や変数定義は利用できない
オペレーション名
デバッグやサーバサイドロギングに有用
関数名と似たようなもの
code:request
query HeroName {
hero {
name
}
}
変数
大抵、クエリに利用したい値は動的に変わる
クエリ内に直接動的な値を埋め込むと、クライアントコードは動的にクエリを生成する必要がある
さらに、それを GraphQL のフォーマットにシリアライズする必要がある
変数
クエリの外部に変数を定義しておくことができる
$ から始めると変数定義になる
query の最初に型とともに定義する (下記の場合型は Episode)
デフォルト値を付加することもできる
code:リクエスト
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
code:変数
{
"episode": "JEDI"
}
ディレクティブ
変数の導入により、手動で文字列をクエリに差し込むことは避けられた
しかし、時には 動的にクエリの構造を変更したい 場合もある
例えば、概要ビューと詳細ビューを持った UI コンポーネントがあったとして、各々に必要なクエリーは異なる
ディレクティブ
フィールド、もしくはフラグメントに含めることができる
GraphQL のコア仕様では、以下の2つが定義されている
@include(if: Boolean) 引数が true の場合のみ、そのフィールドを含める
@skip(if: Boolean) 引数が true の場合、そのフィールドをスキップする
サーバの実装によっては、新しいディレクティブが試験的に実装されている場合がある
code:リクエスト
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
code:変数
{
"episode": "JEDI",
"withFriends": false
}
ミューテーション
GraphQL の議論の数多くは、データのフェッチについて
しかし、サーバーサイドのデータを編集する方法も必要
REST API
リクエストによりサーバサイドに副作用をもたらす場合がある
GET リクエストではそのような場合に使わない、という慣習がある
GraphQL
REST API と同様に、書き込みも行える
書き込みを発生させるオペレーションは、(クエリでなく) ミューテーション として送る、という慣習がある
Query と Mutation の違い
Query は並列に実行される
Mutation は記述した順に、シーケンシャルに実行されることが保証されている
code:リクエスト
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
code:データ
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
code:レスポンス
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
メタフィールド
GraphQL から返ってくる型が何かわからない場合がある
クライアント側で、データ型に応じて処理を行いたい
メタフィールド
クエリ内の任意の場所で利用可能
挿入箇所のオブジェクトの名前等を知ることができる
GraphQL では、フェッチ対象の各種オブジェクトが、デフォルトで具象型等のメタ情報をもっており、それを参照したい場合はメタフィールドを利用する、といった感じ
__typename メタフィールドは、レスポンスに具象型名を返す
code:リクエスト
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
code:レスポンス
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}