GraphQL Query Design
To fully understand the content in this article, you need to be familiar with GraphQL query language.
This article does not describe the GraphQL schema design, it highlights some of the ways to design GraphQL queries based on access patterns. To know about GraphQL schema design, you can learn it from here.
Building Queries in GraphQL
Apart from the major performance benefits from the GraphQL, a single Query in GraphQL can fetch the entire data along with its relationships or fetch a single resource. This ability of GraphQL makes it one of the powerful query languages today.
How can a Single Query return as many numbers of fields as clients ask in one hit?
It's through an object graph (a.k.a schema). GraphQL hands over the control to clients and lets them query the object graph. It either returns a part of an object graph or a complete graph. In order to fully leverage the potential of an object graph, we should bring Graph thinking to data models, and this book provides a great start to migrate to Graph Data models from Relational.
There are certain tips that I discovered that might help while building queries.
- Migrate relational data models to Graph data models to better visualize and identify relationships. This helps to minimize the load on resolvers.
- Identify the number of first-level entities from access patterns, i.e from the vertex where you need to traverse.
- Aggregate all Query filters into a new GraphQL Input type to reuse the input types in many queries.
- Consider keeping the number of queries low so as to reap the maximum benefits of GraphQL. GraphQL resolves fields lazily through resolvers and hence the fields will be resolved only when requested. Hence prefer adding a field to an existing query instead of creating a new query to incorporate a new field in the response.
Note : Decision on the number of queries in the schema actually depends on how many resolvers would be invoked in a single query. If more resolvers are invoked in a single query, you would be better off splitting the queries to reduce the response latency. This also depends on business requirements on how do you want to keep your queries. If you data graph itself is very large, keeping it into single query will probably confuse the clients on the filters you provide. Remember GraphQL is client-first architectural pattern
Let’s try with an Example!
In the Graph Model above, there are four nodes ( Movie, Genre, User, and Actor). Let’s identify the access patterns before creating queries for our model.
- What are the top 20 most rated movies?
- Which all users have rated Movie A that belongs to Genre B and has Actor C acted?
- What are the Genres which contain the top 20 most rated movies?
- Which actors acted in the movie which belongs to a Genre A
For Simplicity, assume our resolvers depend on persistence storage (SQL /NoSQL or Graph databases)to fetch relationships and entities.
As we can see from the Graph model, we could potentially have just one single query users, that could return all movies rated, movies that belonged to genre and actors acted in those movies.
type Query { users(serchBy: UserInput!):[User]}type User { movies:[Movie] name:String}type Movie { actors:[Actor] name:String genre:Genre}type Actor{ name:String
collaborated_actors:[Actor]}type Genre { name:String movies:[Movie]}
Query Request to fetch all users have rated Movie A that belongs to Genre B and has Actor C acted
{
users(searchBy:
{
movie:"A"
genre:"B"
actor:"C"
})
{ name
movies {
name
genre {
name
}
actors {
name
}
}
}
Note that there are three resolvers involved in a single query to fetch movies that users rated and the genre that each movie belongs to and actors who acted in that movie.
However, when we start querying movies, genres, or actors, our resolvers need to spend a lot of time in fetching relationships and what worse can be if you have SQL databases for fetching multi-level relationships.
To serve our access pattern needs, we could split the users' queries into queries from where we would start traversing. Since our access patterns deal with all four entities and expect a response of all four different types ( Movie, Genre, User, and Actor ), we could create four query types. All of them return an array of types.
type Query { users(serchBy: UserInput!):[User] movies(searchBy: MovieInput!):[Movie] actors(searchBy: ActorInput!):[Actor] genres(searchBy: GenreInput!):[Genre]}type User { movies:[Movie] name:String}type Movie { actors:[Actor] name:String genre:Genre}type Actor{ name:String collaborated_actors:[Actor]}type Genre { name:String movies:[Movie]}
Queries
For the four access patterns we have, let's query each of them.
Query Request to fetch top 20 most rated movies
{
movies(searchBy:
{
first:20
})
{
name
}
}
Note that, there are no actors or genre asked and these resolvers will not be invoked.
Query Request to fetch Genres which contain the top 20 most rated movies
{
genres(searchBy:
{
filter : {
lt: movie_rating <= 20
}
})
{
name
}
}
Query Request to fetch actors acted in the movie which belongs to a Genre A
{
actors(searchBy:
{
acted_in_genres:['A']
})
{
name
}
}
With this, we are able to,
- Provide a partial or complete object graph to clients and allow them to traverse and request what fields they require. This is what GraphQL is known for!!
- Minimize the number of root resolvers and there is one resolver for each vertex in our object graph and each of them is aware of how to get edges of that vertex.
- Limit the number of query types and since the number of queries actually represents the number of vertices in our graph, any query can accommodate a new field without creating a new query type for the same.
However, note that this design does not solve the versioning problem as any newly added field will be subjected to change in the future. GraphQL does not solve the API versioning problem, though it provides certain schema directives to deprecate certain fields.
Hope you enjoyed reading this. Happy Learning!! Stay tuned for more articles on GraphQL.