Ahmed Elywa
Posted on May 17, 2020
The best thing about GraphQL. Specifying the requested fields from the client request all the way down to the database.
The problems
One of the most powerful features of GraphQL is the ability for the client to specify the fields returned from the response so that less data is shipped across the network and thus avoiding over-fetching data.
However, are we really doing less work? The backend server and the database still have to do all the work of querying the database, getting all the fields of the requested object(s), and then only return the requested fields through the GraphQL/Network layer.
Also, we have a really big problem facing all of GraphQl servers N + 1 issue.
What is the N+1 Problem in GraphQL?
So we’re only saving network time by shipping a smaller response size, but our backend server and the database are still doing the extra unnecessary work to get all the fields of the requested objects. This is essentially a lot of time wasted that we could potentially optimize.
Solution
Using the same pattern as Facebook's DataLoader, Prisma is caching all queries that happen within one tick and combining the findOne
queries into findMany
where it can. This has a high likelihood of optimizing the queries and allows for individual field resolvers to operate in the case where you have an external API to resolve from.
However, for an application that is mostly based against a single database source, this is a lot of overhead to break apart the query and recombine it, when the query itself could just be run against the data source, guaranteeing that the query you wrote is what gets executed. This avoids all the N+1 problems by not breaking the query apart at all. Avoiding the N+1 problem this way is a pattern sometimes called a root resolver.
In the cases where you would rather just send your graphQL query directly to Prisma to resolve, I've built a new tool to convert the info: GraphQLResolveInfo
object into a select object that can be sent directly to the Prisma Client.
To know more about GraphQLResolveInfo look to @nikolasburk
blog post
GraphQL Server Basics: Demystifying the info Argument in GraphQL Resolvers
Example
We have Prisma Schema
with three models.
model User {
id Int @default(autoincrement()) @id
email String @unique
password String
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
published Boolean @default(false)
title String
author User? @relation(fields: [authorId], references: [id])
authorId Int?
comments Comment[]
}
model Comment {
id Int @default(autoincrement()) @id
contain String
post Post @relation(fields: [postId], references: [id])
postId Int
}
So the normal GraphQL Resolvers
to get one User will be like this:
const resolver = {
Query: {
findOneUser: (_parent, args, { prisma }) => {
return prisma.user.findOne(args);
},
},
User: {
posts: (parent, args, { prisma }) => {
return prisma.user.findOne({where: {id: parent.id}}).posts(args);
},
},
Post: {
comments: (parent, args, { prisma }) => {
return prisma.post.findOne({where: {id: parent.id}}).comments(args);
},
},
}
Let me do GraphQL query to get one user with his posts and comments inside posts and see what is the result:
{
findOneUser(where: {id: 1}) {
id
posts {
id
comments {
id
}
}
}
}
In the GraphQL query, we just need id form every record and what is happening we select all tables fields from DB as you see in the log of queries we have 5 queries to do our request.
prisma:query SELECT `dev`.`User`.`id`, `dev`.`User`.`createdAt`, `dev`.`User`.`email`, `dev`.`User`.`name`, `dev`.`User`.`password`, `dev`.`User`.`groupId` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`published`, `dev`.`Post`.`title`, `dev`.`Post`.`authorId`, `dev`.`Post`.`createdAt`, `dev`.`Post`.`updatedAt`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`authorId` IN (?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` IN (?,?,?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Comment`.`id`, `dev`.`Comment`.`contain`, `dev`.`Comment`.`postId`, `dev`.`Comment`.`authorId`, `dev`.`Comment`.`createdAt`, `dev`.`Comment`.`updatedAt`, `dev`.`Comment`.`postId` FROM `dev`.`Comment` WHERE `dev`.`Comment`.`postId` IN (?,?,?) LIMIT ? OFFSET ?
Ok with my way GraphQL Resolvers
:
import { PrismaSelect } from '@paljs/plugins';
const resolver = {
Query: {
findOneUser: (_parent, args, { prisma }, info) => {
const select = new PrismaSelect(info).value;
return prisma.user.findOne({
...args,
...select,
});
},
},
}
Will do same GraphQL query :
{
findOneUser(where: {id: 1}) {
id
posts {
id
comments {
id
}
}
}
}
And here our db queries log for our request.
First we have just 3 queries so we saved one query for every relation in our request.
second we just select id
from db that we asked in GraphQl query:
prisma:query SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`authorId` IN (?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Comment`.`id`, `dev`.`Comment`.`postId` FROM `dev`.`Comment` WHERE `dev`.`Comment`.`postId` IN (?,?,?) LIMIT ? OFFSET ?
In the End
We have perfect GraphQL server with Prisma And PrismaSelect tool.
You can try my tool with my ready examples in my Pal.js CLI
Conclusion
GraphQL is quite powerful, not only does it optimize performance for client apps, but it can also be used to optimize backend performance, after all, we get the specifically requested fields in our resolver for free.
Posted on May 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.