Chiropractors are trained health care professionals who specialize in the diagnosis and treatment of neuromuscular disorders. They treat the joint pains and educate the patients on improving the same…
Wondering how GraphQL resolvers should share data, and how to reuse them multiple times in your schema? You’ve come to the right place…
For the last couple of weeks, we have been setting up a GraphQL API for one of our customers. While doing that, two major elements took us a while to figure out, and looking around on the web, I could not find this information stated plainly so, here is my attempt to document this for anyone stumbling on the same issues.
To set the table for this problem, let me introduce an example. Let’s say we want to expose the following schema:
To calculate the order total, we need to sum every item and multiple by the item quantity. To do that, we need to fetch the items from the database. Our resolver setup looks like this:
And here comes the first problem we encountered. The total and items resolver both need to fetch the items, so if both are independent, they will both call the database to retrieve the items? So how do we share the information between the two to avoid performing the same query twice? The answer is, we don’t!
The correct answer to this question is that we need a caching layer. Each resolver needs to ask for the data it needs. The first one to ask will trigger the fetching of the required data (rest call, database query, …), while the second one requesting the same data will get it for free because the DataSource will act as a caching layer, at the request level. It’s important to understand that the cache is per-request, not global.
Obviously, if you are not using Apollo, you can still use the same idea, but you will have to implement the same request based caching by yourself.
Let build on our previous example and add another query to our schema
We also need additional resolvers
Let’s try one query on this API and see how it works.
The first thing we need to understand is that for each field in a query, the GraphQL engine will call a resolver, using a breadth-first exploration of the query. If a field doesn’t have a declared resolver, GraphQL will apply a default resolver.
The second thing to understand is that a resolver should never eager-fetch data. It should return an object where the data cost the same to retrieve. Other resolvers are used to retrieve associated data if asked in the query.
Here is the flow of resolvers matching the query elements
#1 — The first field declared in the query is getOrder
. resolvers.getOrder
is called. It fetches the data and returns
Notice that it doesn’t return the order’s total or the items, because to do either, it would need to retrieve the items, which is more costly than retrieving the description and date and we don’t know yet if either field is part of the query. We don’t know if either the Date or Description are required, but since it doesn’t cost more to retrieve them, we do it in one fetch.
#2— The getOrder
resolver returns an Order
type, so the engine will select resolvers.Order
. The next field in the query is date
. resolvers.Order
doesn’t declare a date resolver, so the default resolver applies. The default resolver receives the object returned by the getOrder
resolver (step 1). It tries to find a property in the object with the name of the field in the query, which is date
. Since this field exists, it returns the value 2019–05–04.
Our total
resolver needs to fetch the items to calculate the total, so it calls a dataSource and retrieves the items. It then proceeds to calculate the total by summing all the items’ prices times quantity and return a float.
#4— Our query is done and the resulting object builds from all the leaf-resolver of the breadth-first iteration the GraphQL engine did on the query tree.
Now let’s trace a second query to see the reuse at work. Here is the query:
Here is the flow of resolvers matching the query elements
#1 — The first field declared in the query is getUser
.resolvers.getUser
is called. It fetches the data and returns
Notice that again, it doesn’t return a list of orders, because to do so it would need to retrieve them, which is more costly than retrieving the user’s name and we don’t know yet if this is part of the query. Notice also that we return the name
property, but it doesn’t appear in our query. The default resolver will not be called for the name
property, because it’s not part of the query, so this value will be ignored.
#2— The getUser
resolver returns a User
type, so the engine will select resolvers.User
. The next field in the query is orders
. resolvers.User.orders
is called to resolve. This resolver receives our User
object from step 1. It uses the id
property to retrieve a list of orders and returns
Notice again that we return a description
and date
for each order, but our query only includes the description
. The default resolver will not be called for the date
field, so it will be ignored. As for the description
field, let’s continue to step 3!
#3 — For each order, we need the description
. Since our previous resolver returns a list of type Order
, the engine uses resolvers.Order
.
resolvers.Order
doesn’t declare a description
resolver, so the default resolver is used to use the description field from the object returned at step 2.
#4— The next field is total
. resolvers.Order.total
exists, so it’s called for each order from step 2 and a total
is computed.
#5 — Our final payload is built from the returned values or each leaf-resolvers.
As we can see, the Order
resolver from our first query is reused in our second query.
We could go further and add the possibility to get the User from an Order. To do so, we only have to change our schema to add a user
property to the Order
type and then add a property user
to the Order
resolver.
Update:
I later found a great article from the Paypal team: GraphQL Resolvers: Best Practices. I like their last example since it’s really easy to understand and test, but what you may not realize is that to implement it, you would need to declare a resolver for each fields and never use the default resolver. This would result in a lot of code. I like the approach I describe here better, where the parent resolver is used to resolve the object basic field, and the type resolver is used for relations.
Satu lagi review untuk buku karya Colleen Hoover, penulis novel romantis favoritku. Meskipun baru sekarang bisa baca cerita lama ini, tapi aku akan tetap memberi review yang terbaik. Dua tokoh utama…
Gracias a las precipitaciones, la proyección del rinde mejoró con respecto la semana pasada. Sin embargo, en el sureste de Córdoba, noroeste de Bs As y sur de Santa Fe todavía hace falta más agua. De…