Hi all! Does anyone have any pointers on caching? ...
# apollo-kotlin
s
Hi all! Does anyone have any pointers on caching? I'm trying to cache & re-use the output of a query for multiple items for another query for a single item (going from a list of items to a detail screen for one). Using 3.0.0-alpha07 with a normalized cache. Simplified sample below I've changed the cache key format for an 'entry' to
Entry.<id>
using `ObjectIdGenerator`/`CacheKeyResolver` but I'm getting a cache miss for
EntryQuery
Object 'QUERY_ROOT' has no field named 'entry({"input":{...}})'
, before the inner
Entry
from
EntryOutput
can even be looked up in the cache (which I'd expect to succeed)
Copy code
type Entry {
	id: ID!
	# ... etc
}

query EntriesQuery(...) {
	entries(input: { ... }) { # produces EntriesOutput
		nodes { # [Entry]
			id
		}
	}
}

query EntryQuery($entryId: ID!) {
	entry(input: { entryId: $entryId }) { # produces EntryOutput
		entry { # Entry
			id
		}
	} 
}
m
I'd recommend using
@fieldPolicy
for this instead of writing
ObjectIdGenerator
/
CacheKeyResolver
manuallly. There are sadly no docs yet (it's coming) but there are some integration tests there: https://github.com/apollographql/apollo-android/blob/2ca7d9b7805197ce388c9e96e65ce[…]/commonTest/kotlin/test/declarativecache/graphql/extra.graphqls
Mmmm but
input
is an input object. Not sure if that works already
How do you get the
Entry
id from
input
?
s
Sorry, might have left too much out of my sample! The
input
for the
entry
query has
entryId
m
Thanks! So you'll need to use that in your
CacheKeyResolver
Something like this should do it:
Copy code
object CustomCacheKeyResolver: CacheKeyResolver() {
  override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {
    if (field.name == "entry") {
      val input = field.resolveArgument("input", variables) as? Map<String, Any?>
      val id = input?.get("entryId") as? String
      if (id != null) {
        return CacheKey(id)
      }
    }
    
    // Fallback
    return null
  }
}
s
Ah, that's actually pretty much identical to the custom
CacheKeyResolver
I'm using at the moment, alongside my `ObjectIdGenerator`:
Copy code
val objectIdGenerator = object : ObjectIdGenerator {
    // Called after a network request, when writing to the cache
    override fun cacheKeyForObject(
        obj: Map<String, Any?>,
        context: ObjectIdGeneratorContext,
    ): CacheKey? {
        val typeName = obj["__typename"].toString()

        if (obj["__typename"] == "Entry") {
            val id = obj["id"].toString()
            return CacheKey.from(typeName, listOf(id))
        }

        return null
    }
}
m
Do you mind sharing your
CacheKeyResolver
?
s
Sure:
Copy code
val cacheResolver = object : CacheKeyResolver() {
    override fun cacheKeyForField(
        field: CompiledField,
        variables: Executable.Variables,
    ): CacheKey? {
        return if (field.name == "entry" && field.type.leafType().name == "Entry") {
            val input = field.resolveArgument("input", variables) as? HashMap<*, *>
            return input?.let {
                val entryId = input["entryId"].toString()
                CacheKey.from("Entry", listOf(entryId))
            }
        } else {
            null
        }
    }
}
👍 1
m
Thanks!
Can you check if the non-null return case gets hit?
Might also be worth using
store.readOperation()
directly to rule out an issue in the cache interceptor or something else
s
The non-null return case doesn't get hit with my current
CacheKeyResolver
cacheKeyForField
gets called for
EntryOutput
first, then I get a cache miss, without the chance for
cacheKeyForField
to be called for the inner
Entry
inside
EntryOutput
Thanks for the tip on
readOperation
– I'll check that next!
m
Aaaaahh I think I just got it!
Sorry I missed the
EntryOutput
wrapper type
Damn, that's not an easy one. Your
EntryOutput
type doesn't have an
id
itself
Copy code
query EntryQuery($entryId: ID!) {
	entry(input: { entryId: $entryId }) { # Entry or EntryOutput ?
is entry of Entry or EntryOutput type up there ?
s
Ahh, I hadn't thought about that actually. Another error on my part!
EntryOutput
does actually have an
id
, but I hadn't been querying for it/using it at all –
Copy code
type EntryOutput {
  id: ID
  node: Entry
}
m
Copy code
query EntriesQuery(...) {
	entries(input: { ... }) { # produces EntriesOutput
		nodes { # [Entry]
			id
		}
	}
}
This is weird then... Is that a double list?
Ah no got it,
EntriesOutput
!=
EntryOutput
👍 1
Damn
TBH, I think the easiest path there would be to update your schema to use something relay-like
As it is now, it's super hard to reconcile
QUERY_ROOT.entries
and
QUERY_ROOT.entry
since they have different types
s
Hmm okay, thanks Martin; would you be able to elaborate on a 'relay-like' schema? If it adds any context to this scenario, the
input
for our
EntriesQuery
is has
start
and
end
DateTime
objects at the moment, so we're querying for a range of entries
m
In a lot of schema, you'll find relay connections: https://relay.dev/graphql/connections.htm
Actually, in your specific case, maybe you "just" need to remove
EntryOutput
and return an
Entry
instead. I think that'd do the job
s
That's a good point, thanks for the tip on relay too. I'll look into this some more
👍 1
Came back to this to test some schema changes 😅 Both
EntriesQuery
and
EntryQuery
now return the same type (
EntryConnection
), with cache keys for
EntryNode
and
EntryEdge
in the format
EntryNode:<id>
and
EntryEdge:<id>
respectively. It still doesn't seem possible to reconcile these though – would you have any suggestions on what to try next?
Copy code
type EntryConnection {
  edges: [EntryEdge!]!
}

type EntryEdge {
  id: ID!
  node: EntryNode!
}

type EntryNode {
  id: ID!
  # Other fields left out (title etc.)
}
m
EntryQuery
should return an
EntryNode
. There's no real need for a Connection if it's a single entry
s
That makes a lot of sense actually – made that change, and managed to get our list & detail queries reconciled nicely, thanks a lot for your advice Martin!
m
Nice! Glad to hear it worked out well blob smile happy