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:
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 uniqueid
s across all types.To prevent cache key collisions, cache keys will always have a group identifier component. When the
uniqueKeyGroup
isnil
(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 eachObject
should have the sameuniqueKeyGroup
.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 accesslet 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
oruniqueKeyGroup
).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
:
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.
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?
.
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:
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:
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.
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
.
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:
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.
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.The
object
parameter of this function is anObjectData
struct that wraps the underlying object data. Because cache key resolution is performed both on raw JSON (from a network response) andSelectionSet
model data (when writing to the cache directly), the underlying data will have different formats. TheObjectData
wrapper is used to normalize this data to a consistent format in this context.