Custom cache keys


When working with a normalized cache, it is recommended that you specify a cache ID for each object type in your schema. If you don't, objects are assigned a default cache ID, but that ID can lead to undesirable duplication of data.

The normalized cache computes a cache key for each object that is stored in the cache. With Apollo iOS, you can customize the computation of cache keys to improve the performance and capabilities of your cache.

To learn more, read about how the cache normalizes objects by cache key.

CacheKeyInfo

The information needed to construct a cache key is represented by a CacheKeyInfo value. This struct consists of two properties you can provide to affect how a cache key is computed:

  1. let uniqueKeyGroup: String?

    An optional group identifier for a set of objects that should be grouped together in the normalized cache. This is used as the first component of the cache key.

    Important: Cache key group uniqueness

    All objects with the same uniqueKeyGroup must have unique ids across all types.

    To prevent cache key collisions, cache keys will always have a group identifier component. When the uniqueKeyGroup is nil (the default value), to __typename of the response object is used as the group identifier by default.

    If multiple distinct types can be grouped together in the cache, the CacheKeyInfo for each Object should have the same uniqueKeyGroup.

    Tip: By grouping objects together, their keys in the normalized cache will have the same prefix. This allows you to search for cached objects in the same group by their cache id. To learn more, read about direct cache access

  2. let id: String

    The unique cache ID representing the object. This is used as the second component of the cache key.

    Important: Cache ID uniqueness

    The ID must be deterministic and unique for all objects with the same group identifier (__typename or uniqueKeyGroup).

    That is, the key will be the same every time for a response object representing the same entity in the cache and the same key will never be used for reponse objects representing different objects that also have the same group identifier.

The normalized cache constructs cache keys with the format:

"${GroupIdentifier}:${CacheID}"

Given a CacheKeyInfo:

Swift
1CacheKeyInfo(id: "123", uniqueKeyGroup: "Animal")

Apollo iOS would construct a cache key of "Animal:123".

The SchemaConfiguration file

The SchemaConfiguration file is your entry point to configuring cache keys for the types in your schema.

When Apollo iOS generates code for your project, it will generate a set of metadata types representing the GraphQL schema for your application. One of these files is named SchemaConfiguration.swift.

The code generation engine creates this file if it doesn't exist yet, but never overwrites an existing SchemaConfiguration.swift file. This means you can edit your schema configuration without those changes being overwritten on subsequent code generation runs.

Tip: You can configure the location of the generated schema types with the output.schemaTypes option in your code generation configuration.

Specifying cache IDs

The SchemaConfiguration contains a cacheKeyInfo(for type:object:) function. This function provides you a JSON response object and the concrete type of the object represented by the response.

The object parameter provides a JSON response from either a network request or a cache hit.

The type parameter provides a strongly typed Object type. This is a generated metadata type representing a concrete type in your GraphQL schema.

To configure how cache keys are computed from a response object, you can create and return CacheKeyInfo values from your implementation of cacheKeyInfo(for:object:).

Note: When specifying cache IDs, make sure that you are always fetching the fields used to construct those IDs in your operations. Any response objects that don't contain the cache ID fields will not be able to be merged via cache normalization.

Using a default cache ID field

If your schema provides a common unique identifier across many of your objects types, you may want to use that field as the cache ID by default.

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    guard let id = object["id"] as? String else {
4        return nil
5    }
6
7    return CacheKeyInfo(id: id)
8  }
9}

If the JSON response object has no id field, the function returns nil and the cache will normalize the object using the default response path normalization.

JSON value convenience initializer

Alternatively, you can use the init(jsonValue:uniqueKeyGroup:) convenience initializer. This initializer attempts to use the value of a key in the JSON response, throwing an error if the key does not exist.

If you want to return nil when the value does not exist, you can use try?.

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    return try? CacheKeyInfo(jsonValue: object["id"])
4  }
5}

Specifying cache IDs by Object type

If you would like to specify cache IDs differently for different types of objects, you can use the type parameter.

For an object of the type Dog with a unique key represented by an id field, you may implement cache key resolution as:

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    switch type {
4    case Objects.Dog:
5      return try? CacheKeyInfo(jsonValue: object["id"])
6
7      default:
8      return nil
9    }
10  }
11}

Specifying cache IDs by abstract types

If object types sharing the same interface or union in your schema have the same cache key resolution strategy, you can resolve the key based on those abstract types.

The generated schema metadata includes Interfaces and Unions types that contain a list of all the abstract types used in your GraphQL schema.

For example, for a schema with Dog and Cat types that implement interface Pet, you may implement cache key resolution as:

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    if type.implements(Interfaces.Pet) {
4      return try? CacheKeyInfo(jsonValue: object["id"])
5    }
6
7    return nil
8  }
9}

To instead configure cache key resolution based on a union type, use the union's possibleTypes property.

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    if Unions.ClassroomPets.possibleTypes.contains(type) {
4      return try? CacheKeyInfo(jsonValue: object["id"])
5    }
6
7    return nil
8  }
9}

Grouping cached objects with uniqueKeyGroup

If your cache IDs values are guaranteed to be unique across a number of different types, you may want to group them together in the cache with a common uniqueKeyGroup.

See uniqueKeyGroup for more information on grouping cached objects.

For example, if all objects that implement interface Animal will have unique cache IDs, whether they are Dog, Cat, or any other type that implements Animal, they can share a uniqueKeyGroup.

Swift
SchemaConfiguration.swift
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2  static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3    if type.implements(Interfaces.Pet) {
4      return try? CacheKeyInfo(
5        jsonValue: object["id"],
6        uniqueKeyGroupId: Interfaces.Pet.name
7      )
8    }
9
10    return nil
11  }
12}

Caveats & limitations

Cache key resolution has a few notable quirks and limitations you should be aware of while implementing your cache key resolution function:

  1. While the cache key for an object can use a field from another nested object, if the fields on the referenced object are changed in another operation, the cache key for the dependent object will not be updated. For nested objects that are not normalized with their own cache key, this will never occur, but if the nested object is an entity with its own cache key, it can be mutated independently. In that case, any other objects whose cache keys are dependent on the mutated entity will not be updated automatically. You must take care to update those entities manually with a cache mutation.

  2. The object passed to this function represents data for an object in an specific operation model, not a type in your schema. This means that aliased fields will be keyed on their alias name, not the name of the field on the schema type.

  3. The object parameter of this function is an ObjectData struct that wraps the underlying object data. Because cache key resolution is performed both on raw JSON (from a network response) and SelectionSet model data (when writing to the cache directly), the underlying data will have different formats. The ObjectData wrapper is used to normalize this data to a consistent format in this context.

Feedback

Edit on GitHub

Forums