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:
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:
Source | Directive | Description |
---|---|---|
From a response object's fields (e.g., Book.id ) | @typePolicy | This 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") ) | @fieldPolicy | This 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:
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:
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:
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:
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:
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:
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:
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:
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:
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 theforField
argument.
You can specify multiple key arguments for a field if they're all required to uniquely identify a particular cache entry:
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.