この記事は RevComm Advent Calendar 2023 18 日目の記事です。
はじめに
2023 年 12 月現在、フロントエンド GraphQL クライアントの多くはデータを正規化してキャッシュをする機能を持っています。参考に GraphQL 利用成熟度モデルでは GraphQL のクエリ結果を正規化して活用することは 6 番目に取り上げられていました。
キャッシュと聞くと必ずしも必要な要素ではないように思われるかもしれませんが、もしあなたが API レスポンスを保存して状態管理しているならそれと同じことです。
フロントエンドでの正規化のメリット
正規化とは重複データをなくすことです。正規化された構造でキャッシュするということは、キャッシュ内のすべてのデータが一意であることを意味します。フロントエンド、特に宣言的 UI 下においては、UI に表示するデータソースが宣言されているとき常に最新のクエリ結果が UI に表示されるということを意味します。
正規化されていない場合を考えます。例えば Todo 一覧のクエリ結果に含まれる Todo:1
と Todo:1
単体のクエリ結果が重複して保存されるようになっていると、クエリ発行の間に Todo:1
が更新されている場合、一覧と単体のページで内容が異なるということが起きます。正規化して上書き保存できていれば内容は同じになります。
GraphQL クライアントでの正規化
GraphQL のクエリ結果は、名前のとおりグラフとして構成されているのでノード単位で正規化することができます。GraphQL はスキーマをもとに実装されているので、ライブラリがスキーマから判断して自動で正規化を行うことができます。(Apollo Client の場合はノードの境界を判別するのに __typename
、識別子に id
が使用されます。 id
が存在しない場合はどの値を識別子として扱うかを定義する必要があります。)
Apollo の公式ブログよりクエリ取得から正規化されるまでの図を引用します。
こうして正規化して保存することで、後から Todo 単体を取得した際に自動的に対象のキャッシュを特定して更新することができるのです。さらに、サーバーへ更新処理を行う際も結果として更新のあったノードを返せば GraphQL Client は自動的に対象のキャッシュを特定して更新することもできます。
また、例として簡単な「Todo 一覧」のクエリ結果を取り上げましたが、一般的な GraphQL の使い方として「ユーザーに紐づく Todo 一覧」のようなクエリの結果であってもスキーマをもとに正規化が可能です。
// 正規化前 { id: "user1", __typename: "User", name: "User 001", todos: [ { id: 1, __typename: "Todo", text: "First todo", completed: false } ] }
// 正規化後 // User:user1 { name: "User 001", todos: [ "Todo:1" ] }
// 正規化後 // Todo:1 { text: "First todo", completed: false }
RESTful API での正規化
ここまでの正規化の話は __typename
と id
により非正規化されたデータ(JSON)から正規化されたデータに分解して保存できるというだけの話なので、特に GraphQL である必要はありません。実際に、正規化してキャッシュを保存するという話は GraphQL に限ったものではなく、Redux のドキュメントにも推奨事項として書かれています。
しかし、RESTful API で返す JSON についてモデル毎に __typename
をつけること、一意に識別可能な id
をつけること、と約束を進めるうちに GraphQL のスキーマとクライアントが担っていた機能を再発明することになります。
したがって、最初から GraphQL の仕様に沿って開発を進めていくことが効率的だと考えます。
おわりに
スキーマをもとに実装されるかつスキーマをもとに正規化可能であることはフロントエンジニアにとって GraphQL を使用する大きなメリットになると考えています。最近 GraphQL が React Server Components や BFF と比較されるケースを見ましたが、この特徴は他の技術にはない要素です。 効率よく便利にキャッシュを持てるという点で GraphQL は有用な技術であり続けると考えています。