Referencing Ancestors
One of the common requests that I see in GraphQL is users wanting a way to query an ancestor object from a descendant object. Throughout this article, I'm going to use an example based on the one given in GraphQL spec issue #144; here's the schema under discussion:
type Query {
events: [Event]
}
type Event {
id: ID!
name: String
speakers: [Speaker]
talks: [Talk]
}
type Speaker {
id: ID!
name: String
events: [Event]
talks: [Talk]
}
type Talk {
id: ID!
name: String
speaker: Speaker
event: Event
}
Consider this query:
query {
events {
id
name
speakers {
id
name
talks {
id
name
}
}
}
}
The issue author would like to make it such that >events>speakers>talks (using
operation expressions
syntax) returns the list of talks that the speaker has in the ancestor event.
At first glance, this might seem like a reasonable proposition... However, when
considering graph traversal it becomes apparent why this
would be problematic — the value of a Speaker's talks field should be
independent of the path through which it was accessed. In particular,
normalized stores rely on the graph traversal execution
behaviour outlined in the GraphQL specification; to change this fundamental
behaviour could cause applications using normalized stores to produce subtle
issues resulting in a lot of confusion and frustration.
Breaks normalized stores​
Let's see why normalized stores would be broken by this. If you implement the described change, you might get a result from the request above such as:
{
"events": [
{
"id": "EGQLC",
"name": "GraphQLConf",
"speakers": [
{
"id": "UB3NJ",
"name": "Benjie",
"talks": [
{
"id": "TGFST",
"name": "The Future of Efficiency Is Here: Schema Planning"
}
]
}
]
},
{
"id": "EVRGU",
"name": "VR Gamers",
"speakers": [
{
"id": "UB3NJ",
"name": "Benjie",
"talks": [
{
"id": "TPOPC",
"name": "Mastering Climbing in Population:One"
}
]
}
]
}
]
}
(I've omitted all other speakers and all other events for brevity.)
When you turn this into a normalized store, both events
reference the speaker UB3NJ, who has a field talks that accepts no
arguments; so when you merge these, the latter will overwrite the former, and
the resulting store will end up being something like:
{
"Query": {
"events": [ { "$ref": "EGQLC" }, { "$ref": "EVRGU" } ]
}
"EGQLC": {
"id": "EGQLC",
"name": "GraphQLConf",
"speakers": [ { "$ref": "UB3NJ" } ]
},
"EVRGU": {
"id": "EVRGU",
"name": "VR Gamers",
"speakers": [ { "$ref": "UB3NJ" } ]
},
"UB3NJ": {
"id": "UB3NJ",
"name": "Benjie",
/* This gets overwritten:
"talks": [
{
"id": "TGFST",
"name": "The Future of Efficiency Is Here: Schema Planning"
}
] */
"talks": [
{
"id": "TPOPC",
"name": "Mastering Climbing in Population:One"
}
]
}
}
When you reconstitute this back into data to render you'll end up with Benjie's talk for the "VR Gamers" event being used for GraphQLConf... A subtle issue when testing the software (very hard to notice) but super obvious to the attendees of GraphQLConf who wonder how relevant "Mastering Climbing in Population:One" is to GraphQL!
Solution: rewrite the query​
One solution to this problem is to write your query in such a way that each field can execute in a context-free way and still give you the data you need:
query {
events {
id
name
talks {
id
name
speakers {
id
name
}
}
}
}
Solution: add another field​
Another solution is to perform the grouping on the server side and expose this via the schema:
extend type Event {
speakersAndTalks: [SpeakerAndTalks]
}
type SpeakerAndTalks {
speaker: Speaker
talks: [Talk]
}
which you could query like this:
query {
events {
id
name
speakersAndTalks {
speaker {
id
name
}
talks {
id
name
}
}
}
}
Thanks​
Thank you to fellow TSC member Matt Mahoney for proof-reading this article. Matt comments that this can also occur in:
REST-based resolvers, where the
events.speakerfield is backed by a REST endpoint, which always gives youSpeaker.talksin a context-dependent way.