Apollo Kotlin中的规范化缓存
Apollo Kotlin提供了两个内置的规范化缓存来存储和重用GraphQL操作的
- 一个内存缓存(
MemoryCache
) - 一个SQLite支持的缓存(
SqlNormalizedCache
)
您可以在应用程序中使用其中之一(或两个!)以提高其大多数操作的响应能力。
要开始使用更粗略的缓存策略,速度设置更快,请查看HTTP缓存。
什么是规范化缓存?
在一个 GraphQL 客户端 中,规范化缓存会将每个 GraphQL 操作 响应拆分成包含的各个单独对象。然后,每个对象根据其 缓存 ID 作为 独立的条目 进行缓存。这意味着如果多个响应中包含相同的对象,该对象可以 deduplicated 成单个缓存条目。这降低了整体缓存大小,并有助于保持缓存数据的连贯性和新鲜度。
您还可以将规范化缓存作为 UI 的单一事实来源,使其能够响应缓存中的更改。要了解更多关于 规范化 过程的信息, 请参阅这篇博客文章。
规范化响应
看看这个示例 查询:
query GetFavoriteBook {favoriteBook { # Book objectidtitleauthor { # Author objectidname}}}
这个 查询 返回一个 Book
对象,该对象反过来又包含一个 Author
对象。来自 GraphQL 服务器 的示例响应如下:
{"favoriteBook": {"id": "bk123","title": "Les Guerriers du silence","author": {"id": "au456","name": "Pierre Bordage"}}}
规范化缓存默认不会直接存储此响应。相反,它会将其拆分成以下条目:
"favoriteBook": {"id": "bk123", "title": "Les guerriers du silence", "author": "ApolloCacheReference{favoriteBook.author}"}"favoriteBook.author": {"id": "au456", "name": "Pierre Bordage"}"QUERY_ROOT": {"favoriteBook": "ApolloCacheReference{favoriteBook}"}
ⓘ 注意
这些默认生成的缓存ID(favoriteBook
和favoriteBook.author
)对于数据去重来说是不理想的。请参阅指定缓存ID。
- 注意,
author
字段现在包含字符串ApolloCacheReference{favoriteBook.author}
。这是对Author
缓存条目的引用。 - 注意
QUERY_ROOT
条目也是始终存在的,如果你至少缓存了一个查询的结果。该条目包含对每个包含在查询中的顶层字段(例如,favoriteBook
)的引用。
提供的缓存
内存缓存
Apollo Kotlin的MemoryCache
是一个规范化的、内存缓存,用于存储GraphQL操作的对象。要使用它,首先添加apollo-normalized-cache
artifact到你的build.gradle[.kts]
dependencies {implementation("com.apollographql.apollo:apollo-normalized-cache:4.0.0")}
然后,将其缓存包含在您的 ApolloClient
初始化中,如下所示:
// Creates a 10MB MemoryCacheFactoryval cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)// Build the ApolloClientval apolloClient = ApolloClient.Builder().serverUrl("https://example.com/graphql")// normalizedCache() is an extension function on ApolloClient.Builder.normalizedCache(cacheFactory).build()
由于标准化缓存是可选的,normalizedCache()
是一个定义在 apollo-normalized-cache
元件中的 ApolloClient.Builder()
扩展函数。它以 NormalizedCacheFactory
作为参数,以便在需要时在主线程之外创建缓存。
一个 MemoryCache
是一个最近最少使用(LRU)缓存。它根据以下条件在内存中保持条目:
名称 | 描述 |
---|---|
maxSizeBytes | 缓存的最高大小(以字节为单位)。 |
expireAfterMillis | 现有缓存条目超时的超时时间(以毫秒为单位)。默认情况下,没有超时。 |
当您的应用程序停止时,MemoryCache
中的数据将永久丢失。如果需要持久化数据,可以使用 SQLite缓存。
SQLite 缓存
Apollo Kotlin 的 SQLite 缓存使用 SQLDelight
要启用 SQLite 缓存支持,将 apollo-normalized-cache-sqlite
依赖项添加到您的项目的 build.gradle
文件中:
dependencies {implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.0.0")}
然后根据您的平台目标将 SQLite 缓存包含在您的 ApolloClient
初始化中(不同平台使用不同的驱动器):
// Androidval sqlNormalizedCacheFactory = SqlNormalizedCacheFactory("apollo.db")// JVMval sqlNormalizedCacheFactory = SqlNormalizedCacheFactory("jdbc:sqlite:apollo.db")// iOSval sqlNormalizedCacheFactory = SqlNormalizedCacheFactory("apollo.db")// Build the ApolloClientval apolloClient = ApolloClient.Builder().serverUrl("https://example.com/graphql").normalizedCache(sqlNormalizedCacheFactory).build()
您可以使用 SQLite 缓存的方式与使用 MemoryCache
的方式相同。
链式缓存
为了充分利用标准化缓存,您可以将 MemoryCacheFactory
与 SqlNormalizedCacheFactory
链起来:
val memoryFirstThenSqlCacheFactory = MemoryCacheFactory(10 * 1024 * 1024).chain(SqlNormalizedCacheFactory(context, "db_name"))
每当 Apollo Kotlin 尝试读取缓存数据时,它会按顺序检查每个链式缓存,直到找到匹配项。然后,它会立即返回该缓存数据,而不读取任何其他缓存。
每当 Apollo Kotlin 向缓存写入数据时,这些写入操作将向下传播到链中的所有缓存中。
设置获取策略
在您将标准化缓存添加到您的 ApolloClient
初始化之后,Apollo Kotlin 会自动使用 FetchPolicy.CacheFirst
作为默认(客户端范围) 获取策略 对所有查询。要更改默认值,您可以在客户端构建器上调用 fetchPolicy
:
val apolloClient = ApolloClient.Builder().serverUrl("https://example.com/graphql").fetchPolicy(FetchPolicy.NetworkOnly).build()
您还可以针对特定查询自定义缓存的使用方式,即设置该查询的获取策略。
以下代码片段显示了如何设置所有可用的获取策略及其行为:
val response = apolloClient.query(query)// (Default) Check the cache, then only use the network if data isn't present.fetchPolicy(FetchPolicy.CacheFirst)// Check the cache and never use the network, even if data isn't present.fetchPolicy(FetchPolicy.CacheOnly)// Always use the network, then check the cache if network fails.fetchPolicy(FetchPolicy.NetworkFirst)// Always use the network and never check the cache, even if network fails.fetchPolicy(FetchPolicy.NetworkOnly)// Check the cache and also use the network.fetchPolicy(FetchPolicy.CacheAndNetwork)// Execute the query and collect the responses.toFlow().collect { response ->// ...}
请注意,缓存未命中将产生一个非空的响应,其中包含.exception
,这意味着这些策略中的一些可以产生多个值:
CacheFirst
可以产生1或2个值NetworkFirst
可以产生1或2个值CacheAndNetwork
将产生2个值
注意:.execute()
会过滤掉缓存或网络错误,以返回单个成功响应,然而由于CacheAndNetwork
可以产生2个成功响应,当使用该策略时,你应该使用.toFlow()
。
与normalizedCache(NormalizedCacheFactory)
一样,fetchPolicy(FetchPolicy)
是ApolloClient.Builder()
的扩展函数,因此你需要将apollo-normalized-cache
添加到你的类路径中才能使它生效。
由于规范化缓存可以去除重复的数据,它可以让你对缓存更改做出反应。你通过监视器
来这样做,监视器会监听缓存更改。了解有关查询监视器的更多信息。
指定缓存ID
默认情况下,Apollo Kotlin使用对象GraphQL字段的路径作为其缓存ID。例如,回想以下查询及其之前的结果缓存条目:
query GetFavoriteBook {favoriteBook { # Book objectidtitleauthor { # Author objectidname}}}
"favoriteBook": {"id": "bk123", "title": "Les guerriers du silence", "author": "ApolloCacheReference{favoriteBook.author}"}"favoriteBook.author": {"id": "au456", "name": "Pierre Bordage"}"QUERY_ROOT": {"favoriteBook": "ApolloCacheReference{favoriteBook}"}
现在,如果我们执行一个不同的查询以获取具有相同 Author
id
au456
的对象会怎样?
query AuthorById($id: String!) {author(id: $id) {idname}}}
执行此查询后,我们的缓存看起来像这样:
"favoriteBook": {"id": "bk123", "title": "Les guerriers du silence", "author": "ApolloCacheReference{favoriteBook.author}"}"favoriteBook.author": {"id": "au456", "name": "Pierre Bordage"}"author(\"id\": \"au456\")": {"id": "au456", "name": "Pierre Bordage"}"QUERY_ROOT": {"favoriteBook": "ApolloCacheReference{favoriteBook}", "author(\"id\": \"au456\")": "ApolloCacheReference{author(\"id\": \"au456\")}"}
现在,我们正在缓存两个相同Author
对象的条目!这有几个不好的地方:
- 它占用更多的空间。
- 修改这些对象中的一个不会通知另一个对象的监视器。
我们想通过确保它们在写入时分配相同的缓存ID来去除这些条目,从而得到一个看起来更像这样的缓存:
"Book:bk123": {"id": "bk123", "title": "Les guerriers du silence", "author": "ApolloCacheReference{Author:au456}"}"Author:au456": {"id": "au456", "name": "Pierre Bordage"}"QUERY_ROOT": {"favoriteBook": "ApolloCacheReference(Book:bk123)", "author(\"id\": \"au456\")": "ApolloCacheReference{Author:au456}"}
幸运的是,我们所有的对象都有一个id
字段,我们可以用它来达到这个目的。如果id
在你的graph中对所有对象都是唯一的,你可以直接使用它的值作为缓存ID。如果它对每个对象类型都是唯一的,你可以在这个值前加上类型名称(如上所示)。
方法
有两种方法可以指定对象的缓存ID
- 声明性(推荐)。你可以指定模式扩展,告诉代码生成器在哪些字段中查找ID,并确保在编译时所有这些字段都在操作中请求,以确保所有对象都可以被识别。声明性ID还为每个ID加上类型名称前缀,以确保全局唯一性。
- 程序化.您可以实现自定义API,为对象提供缓存ID。因为您可以执行任意代码,所以此解决方案更加灵活,但也更容易出错,并且要求您的操作请求 关键字段。
清除缓存
调用 apolloClient.apolloStore.clearAll()
以清除所有条目的缓存。注意,如果未配置缓存,调用 apolloClient.apolloStore
将抛出异常。