Apollo Kotlin 中的程序化缓存 ID
在某些情况下声明式缓存 ID不适用于您的用例,您可以为程序化生成您的归一化缓存中对象类型的缓存 ID。
您可以从以下两个来源生成给定对象类型的缓存 ID:
来源 | 类 | 描述 |
---|---|---|
来自响应对象的字段(例如,Book.id ) | CacheKeyGenerator | 这发生在网络请求之后,对于将查询结果与现有缓存数据合并至关重要。这是最常见的情况。 |
来自 GraphQL 操作的参数(例如,author(id: "au456") ) | CacheKeyResolver | 这发生在网络请求之前,使您能够在请求的所有数据已经在缓存中的情况下避免网络往返。这是一种可选的优化,可以避免一些缓存未命中。 |
Apollo Kotlin提供了一个类,可以从这些来源中的每一个生成缓存键。
CacheKeyGenerator
类CacheKeyGenerator
能够使您从对象的字段值生成自定义缓存ID。以下是一个基本示例,从对象的id
字段生成每种类型对象的缓存ID:
val cacheKeyGenerator = object : CacheKeyGenerator {override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {// Generate the cache ID based on the object's id fieldreturn CacheKey(obj["id"] as String)}}
要使用您自定义的CacheKeyGenerator
,请在缓存初始化代码中将其包括,如下所示:
val apolloClient = ApolloClient.Builder().serverUrl("https://...").normalizedCache(normalizedCacheFactory = cacheFactory,cacheKeyGenerator = cacheKeyGenerator,).build()
您可以从context
对象中获取当前对象的typename并将其包含在生成的ID中,如下所示:
val cacheKeyGenerator = object : CacheKeyGenerator {override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {val typename = obj["__typename"] as Stringval id = obj["id"] as Stringreturn CacheKey(typename, id)}}
您还可以使用当前对象的typename,为不同的不同对象类型使用不同的缓存ID生成逻辑。
请注意,为了让缓存ID生成工作,您的GraphQL 操作必须返回自定义代码所依赖的任何字段(如上例中的id
)。如果一个查询没有返回所需的字段,则缓存ID将不一致,导致数据重复。此外,使用context.field.type.leafType().name
可以得到联合的typename,而不是预期在响应中收到的Union的运行时值。相反查询__typename
更安全。为了确保所有操作都包含__typename
,请设置adTypename gradle 配置:
apollo {addTypename.set("always")//}
CacheKeyResolver
缓存键解析器类(CacheKeyResolver)允许您从一个字段的参数中生成自定义缓存键。以下基本示例从id
参数中为每种类型的对象生成缓存键,前提是该参数存在:
val cacheKeyResolver = object: CacheKeyResolver() {override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {// [field] contains compile-time information about what type of object is being resolved.// Even though we call leafType() here, we're guaranteed that the type is a composite type and not a listval typename = field.type.leafType().name// resolveArgument returns the runtime value of the "id" argument// from either the variables or as a literal valueval id = field.resolveArgument("id", variables)if (id is String) {// This field has an id argument, so we can use it to compute a cache IDreturn CacheKey(typename, id)}// Return null to use the default handlingreturn null}}
要使用您自定义的CacheKeyResolver
,您需要在缓存初始化代码中包含它,如下所示:
val apolloClient = ApolloClient.Builder().serverUrl("https://...").normalizedCache(normalizedCacheFactory = cacheFactory,cacheKeyGenerator = cacheKeyGenerator,cacheResolver = cacheKeyResolver).build()
使用自定义CacheKeyResolver
时请注意以下几点:
- 当您的操作返回组合类型时,
cacheKeyForField
函数会对您的每个字段进行调用,因此如果您不想处理某个特定字段,请返回null
。 - 对于返回组合类型列表的字段,函数不会调用。见下文。
处理列表
假设我们有一个这样的查询:
query GetBooks($ids: [String!]!) {books(ids: $ids) {idtitle}}
为了让缓存查找ids列表中的所有图书,我们需要在CacheKeyResolver
中覆盖listOfCacheKeysForField
方法:
override fun listOfCacheKeysForField(field: CompiledField, variables: Executable.Variables): List<CacheKey?>? {// Note that the field *can* be a list type hereval typename = field.type.leafType().name// resolveArgument returns the runtime value of the "id" argument// from either the variables or as a literal valueval ids = field.resolveArgument("ids", variables)if (ids is List<*>) {// This field has an id argument, so we can use it to compute a cache keyreturn ids.map { CacheKey(typename, it as String) }}// Return null to use the default handlingreturn null}
为了简化,我们仅支持列表的一级嵌套。若要支持更深层次的列表,您可以实现CacheResolver
。 CacheResolver
是CacheKeyResolver
的泛化,它可以返回缓存中的任何值,甚至是标量值:
val cacheResolver = object: CacheResolver {override fun resolveField(field: CompiledField,variables: Executable.Variables,parent: Map<String, @JvmSuppressWildcards Any?>,parentId: String,): Any? {var type = field.typevar listDepth = 0while (true) {when (type) {is CompiledNotNullType -> type = type.ofTypeis CompiledListType -> {listDepth++type = type.ofType}else -> break}}// Now type points to the leaf type and lestDepth is the nesting of lists required// Return a kotlin value for this field// No type checking is done here so it must match the expected GraphQL typeif (listDepth == 2) {return listOf(listOf("0", "1"))}// CacheResolver must always call DefaultCacheResolver last or all fields will be null elsereturn DefaultCacheResolver.resolveField(field, variables, parent, parentId)}}