-
Notifications
You must be signed in to change notification settings - Fork 657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to create cache id when paginating #4251
Comments
Hi! We're currently improving the caching/pagination aspect of the library, and this use-case is specifically something we want to support! It would be interesting to see if the experimental APIs we recently added can address your need, but I must warn they are still pretty rough. Automatic way (experimental!)If you’d like to try it:
extend type Patient
@fieldPolicy(forField: "medications", paginationArgs: "period")
private class MedicationsMetadataGenerator : MetadataGenerator {
override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map<String, Any?> {
if (context.field.name == "medications") {
return mapOf("merge" to true)
}
return emptyMap()
}
}
private class MedicationsFieldMerger : FieldRecordMerger.FieldMerger {
override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo {
return if (!existing.metadata.containsKey("merge") || !incoming.metadata.containsKey("merge")) {
incoming
} else {
val existingList = existing.value as List<*>
val incomingList = incoming.value as List<*>
val mergedList = existingList + incomingList
val mergedMetadata = mapOf("merge" to true)
FieldRecordMerger.FieldInfo(
value = mergedList,
metadata = mergedMetadata
)
}
}
}
client = ApolloClient.Builder()
.serverUrl(...)
.normalizedCache(
normalizedCacheFactory = ...,
cacheKeyGenerator = TypePolicyCacheKeyGenerator,
metadataGenerator = MedicationsMetadataGenerator(),
apolloResolver = FieldPolicyApolloResolver,
recordMerger = FieldRecordMerger(MedicationsFieldMerger()),
)
.build() With that, pages should merged in one array and be stored in the cache under one entry, that you can Manual wayYou can use the ApolloStore APIs to manually update the cache with your own manually merged version of the data, right after you retrieve a new page, with a Please don't hesitate tell us what works / doesn't work as this is currently a hot topic for us 😊 |
Thank you for the fast and descriptive response. I will definitely will check both ways and see what suits my needs in this case. |
Hello, I'm trying to make it work on Query level. All seems to go well.
Am I missing something. Or mergeFields does not work on Query ? extend type Query @fieldPolicy(forField: "posts", paginationArgs: "after") override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map<String, Any?> {
if (context.field.name == "posts") {
return mapOf("merge" to true)
} override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo {
// Never resolve with posts here |
Hi @nikonhub ! extend type Query
@fieldPolicy(forField: "posts", paginationArgs: "after")
@typePolicy(embeddedFields: "posts") By the way, if your extend type Query
@typePolicy(connectionFields: "posts") ApolloClient.Builder()
.serverUrl("")
.normalizedCache(
normalizedCacheFactory = cacheFactory,
cacheKeyGenerator = TypePolicyCacheKeyGenerator,
metadataGenerator = ConnectionMetadataGenerator(Pagination.connectionTypes), // Pagination.connectionTypes is generated
apolloResolver = FieldPolicyApolloResolver,
recordMerger = ConnectionRecordMerger,
)
.build() This is still experimental and not documented, but an example is here. |
Thanks for quick reply. I've tried different things. But none worked. I've checked examples here and the source code of ConnectionMetadatGenerator + ConnectionRecordMerger. Then even though I don't use Relay style, I've tried to add connectionFields argument, and got a build error
So now I'm thinkg maybe something wrong in the model generation phase and embeddedFields in typePolicy are not even taken into account. apollo_version = 4.0.0-SNAPSHOT (also tried 3.7.4) implementation "com.apollographql.apollo3:apollo-runtime:$apollo_version"
implementation "com.apollographql.apollo3:apollo-api:$apollo_version"
implementation("com.apollographql.apollo3:apollo-adapters:$apollo_version")
implementation("com.apollographql.apollo3:apollo-normalized-cache-incubating:$apollo_version")
implementation("com.apollographql.apollo3:apollo-normalized-cache-sqlite-incubating:$apollo_version") Sample of schema : interface PagingObject {
data: [Content]
paging: Paging
}
union Content = Post | ...
type PostPagingObject implements PagingObject {
data: [Post!]
paging: Paging
}
type Query
@typePolicy(embeddedFields: "followingCommunitiesPosts")
@fieldPolicy(forField: "followingCommunitiesPosts", paginationArgs: "after")
{
followingCommunitiesPosts(after: String, countryCode: String, latitude: Float, longitude: Float, maxDistance: Float): PostPagingObject
...
Sample of cache dump : "followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).paging" : {
"__typename" : Paging
"cursors" : CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).paging.cursors)
}
"QUERY_ROOT" : {
"followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null})" : {data=[CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.0), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.1), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.2), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.3), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.4), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.5), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.6), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.7), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.8), CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).data.9)], paging=CacheKey(followingCommunitiesPosts({"countryCode":"fr","latitude":null,"longitude":null,"maxDistance":null}).paging)}
}
|
Sorry I forgot to warn that to use extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.2", import: ["@typePolicy", "@fieldPolicy"]) That explains the error you got - but if you you're not using The dump actually looks almost good:
But you should also embed type PostPagingObject @typePolicy(embeddedFields: "data, paging") {
data: [Post!]
paging: Paging
} that way you'll be able to get their values in your |
Thank you a lot ! EmbeddedFields on PostPagingObject was the missing part. |
Hello, Actually I'm rewriting a react native app to native. The basic pattern here is to query network + watch cached data. A later mutation would update all queries watching the changed object. Because data is normalized. This is also the kotlin default behavior. Each item has it's own entry. And mutating an object also updates queries. After adding embeddedFields I haven't realized what were the consequences. Each query now don't reference a stand alone post entry. But keeps all data inside. So it's not possible to mutate objects anymore and watch at the same time. In JS it works almost out of the box with minimal configuration. All fields are "eligible" to go through a custom merge function Query: {
fields: {
followingCommunitiesPosts: {
keyArgs: ['countryCode'],
merge: (previous: any, incoming: any, { args }: any) => {
const { after } = args || {};
if (after) {
return Paging.mergePagingObjects(previous, incoming, {
after,
});
}
return incoming;
},
}, I'm wondering now if it's the right pattern to use with kotlin lib. And even if it's possible to keep all objects as stand alone and customize the merge function for pagination at the same time |
Hi @nikonhub ! I’m expecting |
I'll try to explain from the cache perspective. It should be much clearer Use case :
Without embeddedFields (The default behavior is each entity has it's own record in the database.) Step 1 KEY : followingPosts({"countryCode":"ABC"})
RECORD: {
"data":
[
"ApolloCacheReference{Post:c9ef0915-2240-4dab-8325-a8d66ffefca2}",
"ApolloCacheReference{Post:e836c093-e6f7-4a28-b7e8-b1aa565ddaa0}",
"ApolloCacheReference{Post:045ba7cb-697e-41d2-a0eb-c187b55d8f51}",
"ApolloCacheReference{Post:4ce29af0-a8d4-44f6-b013-57eda45c1e23}",
....
],
"paging": 'ApolloCacheReference{followingPosts({"countryCode":"ABC"}).paging}'
}
...
KEY : Post:c9ef0915-2240-4dab-8325-a8d66ffefca2
RECORD: {
"__typename": "Post",
"id": "c9ef0915-2240-4dab-8325-a8d66ffefca2",
"body": "Content",
"image": null,
"author": "ApolloCacheReference{User:0894f298-ea96-4426-aeb5-08d4f61da2d3}",
"vote": "neutral",
} Step 2
With embeddedFields extend type Query
@fieldPolicy(forField: "followingPosts", paginationArgs: "after")
@typePolicy(embeddedFields: "followingPosts")
extend type PostPagingObject
@typePolicy(embeddedFields: "data paging")
extend type Paging
@typePolicy(embeddedFields: "cursors") Step 1 KEY : QUERY_ROOT
RECORD : {
"followingPosts({"countryCode":"ABC"})": {
"data": [
{
"__typename": "Post",
"id": "c9ef0915-2240-4dab-8325-a8d66ffefca2",
"body": "Content",
"image": null,
"author": "ApolloCacheReference{User:0894f298-ea96-4426-aeb5-08d4f61da2d3}",
"voteType": "neutral"
},
{
"__typename": "Post",
"id": "e836c093-e6f7-4a28-b7e8-b1aa565ddaa0",
...
}
],
"paging": {
"__typename": "Paging",
"cursors": {
"before": "MTY3MzgxNzIyNDM0MA==",
"after": "MTY3MTU0NTQ5OTA5NA=="
}
}
} Step 2
PS : Writing this response I've managed to achieve the expected result... Only embed "paging" without "data". It works in my case because "paging" has enough information to merge 2 objects extend type PostPagingObject
@typePolicy(embeddedFields: "paging") KEY : QUERY_ROOT
RECORD:{
'followingPosts({"countryCode":"ABD"})':
{
"data":
[
"ApolloCacheReference{Post:c9ef0915-2240-4dab-8325-a8d66ffefca2}",
"ApolloCacheReference{Post:e836c093-e6f7-4a28-b7e8-b1aa565ddaa0}",
...
],
"paging":
{
"__typename": "Paging",
"cursors":
{
"before": "MTY3MzgxNzIyNDM0MA==",
"after": "MTY2NTEzNjYzMTIzMA==",
},
},
},
} Meanwhile I've resolved to manually merge objects : But even though the apolloStore readOperation/writeOperation were done on the IO thread it seemed slower (than the mergeFields) and was really cumbersome to call a merge function for each query with pagination. |
Thanks a lot for the explanation and feedback 🙏 That all makes sense. Embedding only what's necessary was the right call, and I'm glad it works out for you now! |
Closing this one for now, don't hesitate to open a new one if other questions come up. |
Hi @BoD 👋
My Normalized Cache has been setup as follows:-
The
When I run the above query the root field for the query i.e Business is merged in the cache as follows:-
when I request for the second page of the query the cache dump for the root field looks as:-
Is this the desired way of merging multiple pages? |
@ArjanSM The pagination arguments are not dropped and the records are not merged. It looks like it is because the extend type Business
@typePolicy(connectionFields: "communityQuestions") Let me know if that works better. |
@BoD Thanks. After extending the
The one thing which may be in interest of developers is to include the |
At the moment this can only be done declaratively but it would probably make sense to have a programmatic API as well. Did you have a specific use-case for this? Don't hesitate to create a new ticket for this (as this one is closed) 🙏 |
I have the following schema
Every time I want to paginate through the medications I pass different number for the period argument so that means every time I call for medications it creates new Cache ID so my cache looks something like.
This is a problem not only because it creates multiple cache results but also I cannot "watch()" the query so when I insert/update/delete it changes.
What I want to achieve is no matter what the period is the cache Id should always be something general like.
Patient:12.medications
I have tried doing something like that, just passing null to keyArgs but nothing happens it still uses the period.
extend type Patient @fieldPolicy(forField: "medications", keyArgs: null)
I also tried doing it programmatically but again, nothing is changing:
Not sure what am I missing and how should I approach this.
The text was updated successfully, but these errors were encountered: