DEV Community

Cover image for Improve Your GraphQL performance with Prisma
Ahmed Elywa for Pal.Js

Posted on • Edited on

Improve Your GraphQL performance with Prisma

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.

Top comments (1)

Collapse
 
jeremiasmarinho profile image
Jeremias Marinho

Hi! I have a problem in pal.js, when I press f11, with the page maximized, the sidebar changes from small to large.
I wish that didn't happen. Do you have any idea how to solve this?