GQLoom

Dataloader

Due to the flexibility of GraphQL, we often need to execute multiple queries when we load an object's associated objects. This causes the famous N+1 query problem. To solve this problem, we can use DataLoader.

The DataLoader is able to reduce the number of queries to the database by merging multiple requests into a single one, and also caches the results of the query to avoid repetitive queries.

Example

Consider that we have the following simple objects User and Book:

import * as  from "valibot"
 
const  = .({
  : .(.("User")),
  : .(),
  : .(),
})
 
interface IUser extends .<typeof > {}
 
const  = .({
  : .(.("Book")),
  : .(),
  : .(),
  : .(),
})
 
interface IBook extends .<typeof > {}

On the Book object, we have an authorID field that references the id field of the User object.

In addition, we need to prepare some simple data:

const : IUser[] = [
  { : 1, : "Alice" },
  { : 2, : "Bob" },
  { : 3, : "Charlie" },
  { : 4, : "David" },
  { : 5, : "Eve" },
  { : 6, : "Frank" },
  { : 7, : "Grace" },
  { : 8, : "Heidi" },
  { : 9, : "Igor" },
  { : 10, : "Jack" },
]
 
const : IBook[] = [
  { : 1, : "The Cat in the Hat", : 1 },
  { : 2, : "Green Eggs and Ham", : 1 },
  { : 3, : "War and Peace", : 2 },
  { : 4, : "1984", : 2 },
  { : 5, : "The Great Gatsby", : 3 },
  { : 6, : "To Kill a Mockingbird", : 3},
]

Let's write a simple resolver for the Book object:

import { , ,  } from "@gqloom/core"
 
const  = .(, {
  : (.()).(() => ),
  
  : (.()).(() =>
    .(() => . === .)
  ),
})

In the above code, we have defined an additional field author for Book objects which will return User objects matching the authorID field. We also define a query called books that will return all Book objects. Here, we use the users array directly to find users. For the following query:

GraphQL Schema
query books {
  books {
    id
    title
    author {
      id
      name
    }
  }
}

We will look up the author field for each Book instance, and in doing so, we will directly traverse the users array to find users that match the authorID field. Here we have 6 Book instances, so we will execute 6 lookups. Is there a better way to reduce the number of queries?

Using the DataLoader

Next, we'll use the DataLoader to optimize our query.

We can use the EasyDataLoader class from the @gqloom/core package for basic functionality, or opt for the more popular DataLoader.

Defining Batch Queries

import {
  ,
  ,
  ,
  ,
  ,
} from "@gqloom/core"
 
const  = (
  () =>
    new (async (: number[]) => {
      const  = new ()
      const  = new <number, IUser>()
      for (const  of ) {
        if (.(.)) {
          .(., )
        }
      }
      return .(() => .())
    })
)
 
const  = .(, {
  : (.()).(() => ),
 
  : (.()).(() =>
    ().(.)
  ),
})

In the code above, we used createMemoization to create a useUserLoader function that returns a EasyDataLoader instance. The memoization function ensures that the same EasyDataLoader instance is always used within the same request.

Inside createMemoization, we directly construct an EasyDataLoader instance and pass a query function. Let's delve into how this query function works:

  1. We pass a batch query function that takes a parameter of type number[] when constructing the EasyDataLoader.

  2. In the query function, we receive an array of authorIDs containing the id of the User object to be loaded. When we call the author field on the Book object, the DataLoader automatically merges all the authorIDs within the same request and passes them to the query function.

  3. In the query function, we first create a Set object, which we use to quickly check if authorID exists in the authorIDs array.

  4. We then create a Map object to store the mapping between authorID and User objects.

  5. Next, we iterate through the users array and add user.id to the authorMap if it exists in the authorIDSet.

  6. Finally, we retrieve the corresponding User objects from authorMap in the order of the authorIDs array and return an array containing those User objects.

It must be ensured that the order of the return array of the query function matches the order of the IDs array. The DataLoader relies on this order to merge the results correctly.

In this way, we can use the useUserLoader function in BookResolver to load the author field of the Book object. When calling the author field for all 6 Book instances, DataLoader automatically merges these requests and iterates through the users array only once, thus improving performance.

On this page