GraphQL のクライアント導入におけるメリット享受レベル
GraphQL のメリットを享受するにあたって、いくつかの側面を適用しやすい順に段階的に考えることができると思ったので書き出してみる。
レベル 1: 型とドキュメントのある REST API
GraphQL を利用すると最低でも勝手にこの状態になる。
ここでは、REST API からの移行を重視し、1つのクエリで必要なデータをすべて取ることにこだわらず、REST API を叩くのと同じ単位で GraphQL API を叩く。
例えば、今まで /users を叩いていたのであれば、それと同じ形で返ってくるように /graphql に query { users { id, name, etc } }
というパラメータで同じタイミングで投げる。
これであれば、同等の REST API とサーバの負荷も同じで[1]、クライアントの実装変更も最低限に抑えつつ、型とドキュメントが得られる。
GraphQL のメリットとしてよく挙げられる、「API 通信の無駄の防止」はできないものの、悪い REST API よりはマシな状態に持っていける。
この段階では、型を生成さえできればランタイムではクライアントライブラリは不要で、API レスポンスの入れ先 (例えば React State や Redux) も REST と同じものを使えばよい。
メリット
GraphQL Schema から型を生成できる
GraphQL code generator を利用することで、プラグインがある言語については型情報を生成することができる。
クエリ試験ツール、ドキュメントビュアーが手に入る
GraphiQL や Altair を使うことで、クエリの試験実行やドキュメントの閲覧も行うことができる。
ドキュメントに説明がしっかり書かれていなくても、最低限の型とネスト構造がわかる。
API 提供者が GraphiQL などをホスティングしてくれない場合にでも、Chrome extension などを活用することで、任意の GraphQL エンドポイントに任意のヘッダを設定した状態で試す事が可能。
導入までの課題
サーバ
まず、チームとして GraphQL を導入する判断ができるかが挙げられる。特に、Dataloader や Resolver といった考え方は今まで REST API 実装ではしてこなかったと思われる[2]。 こういった概念をチームに導入することが主な課題となる。また、current_user をはじめとした実行コンテキストの取得方法や認可のやりかたを GraphQL ライブラリでどう実現するかについても学ぶ必要がある。
また、部分導入中の GraphQL Resolver と REST API との処理重複部分をいかに共通化するかという問題もある。
クライアント
クライアントの型を生成するにあたって、生成されるコードにランタイム依存がない (あるいは薄い) ライブラリはあるか。TypeScript はランタイム依存なしのものがあるが、それ以外の言語については、ランタイム依存のないor薄い実用的なものが乏しい可能性がある。
型を生成する際に注意すべき点として、型は 「GraphQL schema に対して」ではなく、「GraphQL operation (query/mutation/subscription)」に対して生成する点がある。GraphQL schema に対してしか生成できないライブラリや、あるいは operation に対応した型を生成できたとしても、schema 側も生成されている場合があるため、そちらを使ってしまうことのないように注意。
レベル 2: UI コンポーネントツリーと対応したクエリを親が組み立てて、1度だけリクエストを投げる (Fragment Colocation)
GraphQL で言われる「API 通信の無駄の防止」は、ただ GraphQL を使うだけでは実現できない。GraphQL クエリの組み立てが、それを必要とするコンポーネントとバラバラになっている場合、そのクエリのメンテがされなかったときに結局 over fetching/under fetching が生じる。そのクエリが必要最低限であり続けるためには、各コンポーネントが、自分が必要な Fragment を宣言し、それを親が束ねる Fragment Colocation が必須となる。
Fragment Colocation については
https://gist.github.com/Quramy/566ea87d0121ceb8cd97ad9d14b63fd8#3-composition が詳しい。
この段階でも、型を生成さえできればランタイムではクライアントライブラリは不要で、API レスポンスの入れ先も REST と同じものを使えばよい。
メリット
Over fetching、under fetching が現在、将来にわたって防げる。
導入までの課題
サーバ
レベル1より大きく、複雑なクエリにサーバが対処できるよう、適切に実装する必要がある。
クライアント
レベル1と異なり、実際に使うのは「Schema の型」でも、「クエリの結果に対する型」でもなく、「各コンポーネントで宣言した Fragment に対する型」である。コンポーネントに fragment 通りのデータを与えるにあたっての型変換が、言語によっては難しい/手間がかかる可能性がある。型生成ライブラリの質に依存する。
レベル 3: キャッシュ活用
レベル 1 と 2 では、型の生成ライブラリの質には依存するものの、HTTP さえ喋ることができれば GraphQL 固有のクライアントライブラリは不要だった。
レベル 3 は、キャッシュを利用することで更新 API のレスポンスを待つことなく UI の更新を行ったり (Optimistic UI)、部分的や、完全なるオフライン対応が可能になる。
REST API の場合、キャッシュを考慮すると、別の API が同一のリソースを返した際にまとめて扱えるようにするために正規化が必要だった。
(Redux の例: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape)
例えば、/users/1 が
{user: {id: 1, name: 'foo', email: 'foo@example.com'}}
/articles/1
{article: {title: 'hello', author: {id: 1, name: 'foo', url: 'example.com'}}}
だとしたとき、管理者がユーザ 1 の名前を変更したとする。
このとき、/users/1 や /articles/1 を必要に応じて都度叩かずキャッシュする戦略をとると、user.name と article.author.name はどちらも更新が必要である。
これを回避するために、例えばどちらも user も author も users 配下に束ね、ここで一括管理することが考えられる。
{users: {1: {name: 'foo', email: 'foo@example.com', url: 'example.com'}}}
GraphQL も同様に、ネストレベルが異なる同一リソースを返す場合がある。
query {
user { id, name, email },
article { title, author { id, name, url }
}
利用する GraphQL クライアントライブラリやそのオプションにより異なるが、この正規化は自動で行われる。
例えば Apollo のデフォルトでは、__typename
と id が :
で連結したものになる。
{"users:1": {name: 'foo', email: 'foo@example.com', url: 'example.com'}}
メリット
自分で正規化などをしなくとも、クライアントライブラリが導入できれば Optimistic UI などの実現ができる。
導入までの課題
サーバ
クライアントキャッシュの話なのでなし。
クライアント
まず、そもそもステートフルなクライアントライブラリを導入できるかという問題がある。
既になんらかのアプリケーショングローバルな状態管理手法が導入されている場合、それと衝突、あるいは何がどちらに入っているかわからないという混乱を招く可能性がある。
また、キャッシュを適切に更新、無効化することは難しい[3]。クライアントライブラリのキャッシュの挙動を理解していない場合、古い状態が見えてしまう可能性がある。
-
同じ形で返すために、DB などから引っ張ってきたり、それを加工したりするコストは同じはず。Graphql query の parse のコストが余計にかかるものの、それ以外の部分のほうが支配的であると考えられる。 ↩︎
-
愚直なループを束ね、あとでまとめて評価させるような考え方の導入は REST API でも可能ではある。例としては https://github.com/exAspArk/batch-loader がある。 ↩︎
Discussion