Declarative cache IDs in Apollo Kotlin


When using a normalized cache in Apollo Kotlin, it's 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.

When specifying cache IDs, it's recommended that you do so declaratively, using the methods described in this article:

GraphQL
1extend type Book @typePolicy(keyFields: "id")

For advanced use cases, you can also define cache IDs programmatically.

How they work

With declarative cache IDs, the codegen process adds ID fields automatically and generates type-safe code that can return a valid cache ID for any object.

To do this, Apollo Kotlin extends your backend schema. The GraphQL spec supports object and interface extensions using the extend keyword. We can use this together with the @typePolicy and @fieldPolicy directives, along with an extra .graphqls file named extra.graphqls.

Note that you'll need to make sure that the extra.graphqls file is included in your schema configuration.

You can obtain a given object type's cache ID from one of two sources:

SourceDirectiveDescription
From a response object's fields (e.g., Book.id)@typePolicyThis happens after a network request and is essential to merging a query result with existing cached data. This is the most common case.
From a field's arguments (e.g., author(id: "au456"))@fieldPolicyThis happens before a network request and enables you to avoid a network round trip if all requested data is in the cache already. This is an optional optimization that can avoid some cache misses.

@typePolicy

The @typePolicy directive enables you to specify an object's cache ID from key fields of the object returned by your GraphQL server. Most commonly, you can use an object's id field as its key field.

For example, let's say our app's schema.graphqls file includes the following definition:

GraphQL
schema.graphqls
1type Book {
2  id: String!
3  author: Author!
4  title: String!
5}

We can add the following definition to an extra.graphqls file in the same directory as our schema:

GraphQL
extra.graphqls
1extend type Book @typePolicy(keyFields: "id")

Apollo Kotlin now knows to use the id field of a Book type to generate its cache ID. A cache record may now look like:

Text
1"Book:bk123": {"id": "bk123", "title": "Les guerriers du silence", "author": "ApolloCacheReference{favoriteBook.author}"}

You can specify multiple key fields for an object if they're all required to uniquely identify a particular cache entry:

GraphQL
extra.graphqls
1extend type Author @typePolicy(keyFields: "firstName lastName")

In this case, the cache ID for an Author object includes the values of both its firstName and lastName fields:

Text
1"Author:PierreBordage": {"id": "au456", "firstName": "Pierre", "lastName": "Bordage"}

It is also possible to use @typePolicy on interfaces. This specifies the key fields for all the implementing types:

GraphQL
extra.graphqls
1extend interface Node @typePolicy(keyFields: "id")

All of an object type's key fields must return a scalar type.

Note that the key fields specified this way will automatically be added during code generation to selections on the object type, since they are always needed to identify the object in the cache. This means you don't need to include them in your queries.

Adding __typename to your operations

In addition to the key fields, the declarative cache requires the __typename of each object by default. These typenames are not included by default as they make query larger for non-cache users. To avoid cache misses, add __typename to every selection using addTypename:

Kotlin
1apollo {
2  service("service") {
3    addTypename.set("always")
4  }
5}

@fieldPolicy

The @fieldPolicy directive enables you to specify an object's cache ID from the values of key arguments you provide to a particular field. This enables you to identify an object in your cache before sending a network request, potentially enabling you to skip the request entirely.

For example, let's say our app's schema.graphqls file includes the following definition:

GraphQL
schema.graphqls
1type Query {
2  book(id: String!): Book
3}

We happen to know that this query returns whichever Book object has an id field that matches the required argument. Therefore, we can make the id argument a key argument for this field.

We can add the following definition to an extra.graphqls file in the same directory as our schema:

GraphQL
extra.graphqls
1extend type Query @fieldPolicy(forField: "book", keyArgs: "id")

Apollo Kotlin now knows to check the cache for a Book object with the provided id before sending a network request for Query.book.

Note that even though the @fieldPolicy directive corresponds to a single field, you apply the directive to the type definition (Query in this case). This is because GraphQL doesn't allow extending a single field. You specify which field the directive corresponds to with the forField argument.

You can specify multiple key arguments for a field if they're all required to uniquely identify a particular cache entry:

GraphQL
extra.graphqls
1extend type Query @fieldPolicy(forField: "author", keyArgs: "firstName lastName")

In this case, the cache ID for an Author object includes the values of both its firstName and lastName fields, which are both provided as arguments to the Query.author field.

If multiple fields of an object type have key arguments, you can apply multiple @fieldPolicy directives to that type.

Feedback

Edit on GitHub

Forums