Chatwork Creator's Note

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

apollo-ios が提供するキャッシュの基本機能

初めまして!2023年7月よりChatworkでモバイルアプリケーション開発部でiOSアプリ開発をしているterryです。

本記事はChatwork Product Day 2023の応援記事です。

lp.chatwork.com

現在ChatworkではREST APIからGraphQLへのリプレイスを進めています。各プラットフォーム毎に実装方針を策定しており、iOSアプリプラットフォームチームではGraphQLのクライアントライブラリの選定において現状最も利用されている「apollo-ios」を使うことにしました。

github.com

apollo-iosには強力なキャッシュ機能があり、 ChatworkのiOSアプリでは既存のキャッシュ機構(Realm)との置き換えが可能かどうか調査しましたので、今回はその基本機能を紹介したいと思います。

キャッシュの種類

apollo-iosには2種類のキャッシュが標準で用意されています。 ここではそれぞれの特徴を挙げていきたいと思います。

InMemoryNormalizedCache

メモリ上でキャッシュ

  • デフォルトのキャッシュ
  • アプリケーションの実行中に正規化された結果をシステムメモリに直接保存
    • アプリケーションの終了時にキャッシュが保存されない
    • ディスクベースのキャッシュ実装よりもキャッシュへのレコードの読み取りと書き込みが高速
  • 有効期間の短いデータのキャッシュに最適
    • 頻繁に変更されることが予想されるデータ、または将来再びアクセスされる可能性が低いデータ

SQLiteCache

DB上でキャッシュ

  • ApolloSQLiteライブラリをimportする必要がある
  • アプリ内に SQLite ファイルを作成
    • アプリのサイズが大きくなる
    • データを永続化できる
    • キャッシュの応答時間がわずかに増加
    • キャッシュがメモリを過剰に使用するリスクがない
  • 存続期間の長いデータのキャッシュに最適
    • 頻繁に変更されることが予想されないデータ
    • またはオフラインでもアクセスできる必要があるデータ

その他のカスタムキャッシュ

protocol NormalizedCacheに準拠させることで、独自のキャッシュ機構を構築することもできそうです。 (試せておりません)

キャッシュの制御

  • キャッシュから取得するのか、サーバーから取得するのか
  • キャッシュへの保存をするのか、しないのか

どのようなアプリのおいてもキャッシュの利用を制御したいケースは発生すると思います。

例えば単一の挙動のみだと下記のような問題が発生するかもしれません。

  • 基本的にはキャッシュがあればキャッシュから取得する
  • 強制的に更新したいケースもある
  • オフライン時など通信できない時はキャッシュから取得する

上記の要件についてキャッシュがあればキャッシュから取得するという挙動のみで実装する場合、キャッシュがある限りは更新できないので、もし強制的に更新したい時はキャッシュをクリアする必要があります。 しかしキャッシュをクリアすると通信できない時はキャッシュから取得するという要件が実現できそうにありません。

つまりユースケース毎にキャッシュの利用を制御する必要があります。

ApolloClientではfetchメソッドの引数にCachePolicyというenumを渡すことで、キャッシュの挙動を制御できます。 ApolloClient生成時にCachePolicyを渡すのではなく、fetchメソッドの引数で渡すところが使いやすいポイントです。 そのおかげでQueryやMutation毎にCachePolicyを使い分けることでき、柔軟なユースケースを実現できます。

CachePolicyの種類

CachePolicyは5種類あります。

  1. case fetchIgnoringCacheCompletely

    結果を常にサーバからフェッチし、キャッシュに保存しない。

  2. case fetchIgnoringCacheData

    常にサーバから結果をフェッチする。

  3. case returnCacheDataAndFetch

    利用可能であればキャッシュからデータを返し、常にサーバから結果をフェッチする。

  4. case returnCacheDataDontFetch

    利用可能であればキャッシュからデータを返し、そうでなければエラーを返す。

  5. case returnCacheDataElseFetch

    利用可能であればキャッシュからデータを返し、そうでなければサーバーから結果をフェッチする。

CachePolicyを使った解決

これら5つのCachePolicyを使いこなすことで様々なユースケースを実現できそうです。 例えば先ほどの単一の挙動で実装して問題が発生した例を解決する方法を考えてみます。

キャッシュがあればキャッシュから取得する

→ 5. case returnCacheDataElseFetch

強制的に更新したい

→ 2. case fetchIgnoringCacheData

通信できない時はキャッシュから取得する

→ 4. case returnCacheDataDontFetch


このようにほとんどの場合CachePolicyを組み合わせることで解決できると思います。 しかしこれらはキャッシュを使う使わないに限った制御で、「一部だけキャッシュを更新したい」などキャッシュ機構のカスタマイズはできません。

もっと柔軟に使いたい場合

ApolloStoreを利用するとキャッシュをカスタマイズすることが可能です。

withinReadTransaction

ReadTransactionを使ってRead操作ができる。

withinReadWriteTransaction

ReadWriteTransactionを使ってWrite, Remove, Update操作ができる。

かなり自由な操作ができてしまうので、カスタマイズを使うかどうかはプロジェクトの方針で決める必要があると思います。

注意事項

いずれの方法もキャッシュからのRead、Writeが非同期的に動くことも考慮に入れる必要があります。 特にキャッシュから同期的にデータを取得してファーストビューを表示しているアプリでは注意が必要です。

まとめ

キャッシュ機構はアプリのパフォーマンスや挙動に少なからず影響するので、既存のキャッシュ機構との置き換えを検討する場合は注意深く吟味する必要があります。 iOS開発においてGraphQLを扱う場合、デファクトスタンダードになりつつあるApolloのライブラリを検証する機会が出てくると思いますので、この記事が少しでもお役に立てれば幸いです。

またサンプルプロジェクトも公開しております。 github.com

※挙動の確認にはGitHubのプライベートアクセストークンを作成する必要があります。


最後に

ChatworkのiOS開発チームでGraphQLへのリプレースに挑戦したい人を探しています!

hrmos.co