【前編】Apollo Clientのキャッシュから考えるサーバー設計 | 株式会社トリドリ | 急成長企業を支援してきたマーケティング会社が厳選した急成長企業と出会える場所「LEAPLACE」 - LEAPLACE
LEAPLACE
アプリでより快適にご利用できます
アプリで開く
LEAPLACE

【前編】Apollo Clientのキャッシュから考えるサーバー設計

学び・ノウハウ共有
- 業務内容
見出し画像

はじめまして!
株式会社トリドリ・バックエンドエンジニアの鎗水です。
ここ最近は、TypeScript+NexusでGraphQLサーバーを書いています。


(前編と後編に分けてお話しています。是非後編もご覧ください。)



目次

  1. はじめに
  2. GraphQLとは
  3. ApolloClientのキャッシュの仕組み
  4. キャッシュの正規化




はじめに


 皆さん、GraphQLのキャッシュは使っていますか?
GraphQLのキャッシュはフロントエンドの話と思われがちですが、キャッシュの性能を発揮するにはバックエンドエンジニアもその仕組みを理解することが重要になってきます。
本記事では、キャッシュの性能を発揮するために、バックエンドエンジニアがGraphQLサーバー構築で考慮すべき点を紹介します。


※ クライアントライブラリは、Apollo ClientやRelayなどがありますが、本記事ではApollo Clientを前提とします。


GraphQLとは

 GraphQLとはAPIのためのクエリ言語であり、そのクエリを実行するためのランタイムです。
クライアント側が必要とする情報のみをクエリ言語でリクエストし、取得することができます。
これにより、レスポンスのデータ量をコントロールし、削減できます。


ApolloClientのキャッシュの仕組み

 サーバーから受け取ったレスポンスは、ROOT_QUERYオブジェクトに追加された状態でキャッシュに保存されます。
例えば、Userを取得する場合は、以下のような流れになります。


  • 初回
    1. クライアントはid:5のUserをリクエストします。
    2. キャッシュにid:5のUserが存在するか確認します。
    3. 初回はキャッシュに存在しないため、サーバーへUserの取得を問い合わせます。
    4. サーバーから取得したUserはキャッシュされます。
    5. Userをクライアントに返却します。


  • 2回目以降
    1. クライアントはid:5のUserをリクエストします。
    2. キャッシュにid:5のUserが存在するか確認します。
    3. サーバへは問い合わせずに、クライアントにUserを返却します。

画像

この時、ROOT_QUERYにはフィールド名(引数)をキーとするオブジェクトとして追加されます。

▼キャッシュに保存されたオブジェクト

{ "ROOT_QUERY": { // フィールド名(引数)がキーになる"user(id:5)":{ "id": 5, "name": "ユーザー5", } } } copy

キャッシュの正規化

GraphQLのクエリは、親オブジェクトに加えて、それに関連する子オブジェクトも指定することができます。
実際には、さらに複雑なクエリによるデータ取得が行われるでしょう。
複雑なデータをそのままキャッシュに保存してしまうと、同じidを持つ似通ったオブジェクトを複数保持してしまうことになり、キャッシュの重複につながります。


▼重複があるキャッシュ

{ "ROOT_QUERY":{ "users":[ { "id": 1, "name": "ユーザー1", "posts": [ // 重複しているオブジェクトが存在する {"id": 1,"title": "投稿1","body":"投稿内容","_typename": "Post"}, {"id": 2,"title": "投稿2","body":"投稿内容","_typename": "Post"} ], "__typename": "User" }, { "id":2, "name":"ユーザー2", "posts":[ // 重複しているオブジェクトが存在する {"id":1,"title":"投稿1","body":"投稿内容","tag":"技術記事","_typename":"Post"} ], "__typename":"User" } ] } } copy


キャッシュの重複はできる限り避けるべきです。
Apollo Clientは自動的にオブジェクトを最小単位で切り出し、保存する機能(正規化)によって、この問題を解決しています。


切り出したオブジェクトは__typename:idをキーとして保存され、__ref:__typename:idから参照されます。
GraphQLの特性上、idが同じオブジェクトでも保持しているフィールドが異なることがあります。
Apollo Clientはこのようなオブジェクトを1つにまとめて正規化してくれます。


▼正規化されたキャッシュ

{ "ROOT_QUERY": { // フィールド名がキーになる"users": [ {"__ref":"User:1"}, {"__ref":"User:2"} ] } // 「__typename:id」をキーとしたオブジェクト"User:1": { "id": 1, "name": "ユーザー1", "__typename": "User", "posts":[ {"__ref":"Post:1"}, {"__ref":"Post:2"} ] }, "User:2": { "id":2, "name":"ユーザー2", "__typename":"User", "posts":[ {"__ref":"Post:1"} ] }, "Post:1":{"id":1,"title":"投稿1","price":1000,"_typename":"Post"}, "Post:2":{"id":2,"title":"投稿2","_typename":"Post"}, } copy


そのため、GraphQLサーバーから__typenameとidが返却されることは重要です。
仮にサーバー側から返却されなかったとしても、クライアント側でキーを独自に設定できますが、可能な限りデフォルトの設定に従うことをお勧めします。


後編へ続きます。