Using DataLoader with GraphQL: A Concrete Example

John Tucker
codeburst
Published in
6 min readSep 5, 2018

A fairly technical example (TypeScript) on using the DataLoader library to (magically) solve a very specific problem that comes up in implementations using GraphQL.

First, this gets at a very specific problem that comes up in implementations using GraphQL; so in order to understand the problem you have to have a decent understanding of GraphQL.

Second, while the problem comes up regardless of how one accesses the data, e.g., direct database access or through an ORM, the example here is built on top of TypeORM as it makes the code readable. You should be able to follow along even if you are unfamiliar with TypeORM.

Finally, I will defer to another article to explain the problem and the theory of the solution.

Assuming that you roughly understood the problem and the theory, here is a concrete illustration of the problem and then the solution.

The final solution is available for download.

Initial Database API Setup

In order to illustrate the use of DataLoader, we need to do a fair amount of initial setup; primarily setting up of a database and an API to access it from Node.js. Just so happens, I just finished a series of articles, TypeORM By Example: Part 1, that provides us the foundational knowledge to easily accomplish this.

Specifically, we will be:

  • Using a local Docker PostgreSQL database with an application-specific database and user account
  • Starting with a TypeScript, Node.js, and TypeORM project with an existing data model and seed data

The data model consists of a Post and a Tag with a many-to-many relationship between them. The seed data consists of 10 Posts and 3 Tags; Posts have between 0 to 3 associated Tags.

note: The project was developed using Node.js version v8.11.4.

We begin by downloading the initial project and with the database up and running execute:

npm install
npm run build-ts
npm run start

At this point the Initialize migration will have run to create the database schema for the application. The resulting table structure is:

hellodb=> \dtList of relations
Schema | Name | Type | Owner
--------+---------------+-------+-----------
public | migrations | table | hellouser
public | post | table | hellouser
public | post_tags_tag | table | hellouser
public | tag | table | hellouser
(4 rows)

The migrations table is internal to TypeORM. The post and tag tables are fairly self-explanatory. The post_tags_tag table defines the many-to-many relationship between posts and tags.

hellodb=> \d post_tags_tagTable "public.post_tags_tag"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
postId | integer | | not null |
tagId | integer | | not null |
Indexes:
"PK_e9b7b8e6a07bdccb6a954171676" PRIMARY KEY, btree ("postId", "tagId")
Foreign-key constraints:
"FK_41e7626b9cc03c5c65812ae55e8" FOREIGN KEY ("tagId") REFERENCES tag(id) ON DELETE CASCADE
"FK_b651178cc41334544a7a9601c45" FOREIGN KEY ("postId") REFERENCES post(id) ON DELETE CASCADE

We then seed the database:

npm run seed

TypeORM Behaving Well

We first demonstrate that TypeORM itself does not suffer the N+1 problem. Specifically, the following updates to the initial project provides a list of Posts including their associated Tags.

src / server.ts

src / postManager.ts

Observations:

  • The core functionality is provided in the find({ relations: [‘tags’] }) method call

Building and running the application:

npm run build-ts
npm run start

Produces the output:

The result is accomplished under the hood with a single SQL call; thus not suffering from the N+1 problem.

SELECT "Post"."id" AS "Post_id", "Post"."name" AS "Post_name", "Post_tags"."id" AS "Post_tags_id", "Post_tags"."name" AS "Post_tags_name" 
FROM "post" "Post"
LEFT JOIN "post_tags_tag" "Post_Post_tags"
ON "Post_Post_tags"."postId"="Post"."id"
LEFT JOIN "tag" "Post_tags"
ON "Post_tags"."id"="Post_Post_tags"."tagId

GraphQL Behaving Badly

We now overlay a GraphQL implementation on top of our TypeORM application. The core setup of the GraphQL implementation is documented in the first article in the series: GraphQL Pagination By Example: Part 1.

We then add two functions, allPosts and tagsOfPost, to:

src / postManager.ts

and update:

src / server.ts

Observations:

  • In order to match up with the flexibility of the queries, the resolver structure is structured granularly; i.e., we require the ability to return just the Posts or Posts with their Tags
  • Additionally, for maximal flexibility we are required to be able to return the Tags of a single Post; e.g., the resolver to return Tags cannot make assumptions on how many Posts are returned (maybe none, one, or maybe all)

The query to return just Posts behaves nicely, i.e., under the hood a single database call is made.

SELECT "Post"."id" AS "Post_id", "Post"."name" AS "Post_name" FROM "post" "Post"

The query to return Posts with their Tags behaves badly; N + 1 problem. In this case it makes 11 database calls.

SELECT "Post"."id" AS "Post_id", "Post"."name" AS "Post_name" FROM "post" "Post"SELECT "Post"."id" AS "Post_id", "Post"."name" AS "Post_name", "Post_tags"."id" AS "Post_tags_id", "Post_tags"."name" AS "Post_tags_name" FROM "post" "Post" LEFT JOIN "post_tags_tag" "Post_Post_tags" ON "Post_Post_tags"."postId"="Post"."id" LEFT JOIN "tag" "Post_tags" ON "Post_tags"."id"="Post_Post_tags"."tagId" WHERE "Post"."id" = $1 -- PARAMETERS: [12]SELECT...

DataLoader to the Rescue

Having identified the specific problem, let us go ahead and solve the problem. We first install DataLoader.

npm install dataloader

I found it challenging to understand using DataLoader without a concrete example in mind; here we give a concrete example. The key to using DataLoader is writing:

A batch loading function accepts an Array of keys, and returns a Promise which resolves to an Array of values.

— DataLoader — DataLoader

In our specific case, we need to provide a function that accepts an array of Post ids that returns a promise that resolves to an array of arrays of Tag objects; the outer array is by Post and the inner array are the Tags of a particular post.

src / postManager.ts

With this in place we simply replace the function that returns a single Tag with the DataLoader version.

src / server.ts

Observations:

  • The result of the tagLoader.load(post.id) call is a promise that resolves with the Tags for the specific Post
  • The magic, however, is that tagLoader will accumulate (batch) multiple such requests; waiting for the next process tick to execute the underlying tagsOfPosts function (returns a single promise with all the data)

With this in place, the query to return Posts with their Tags behaves well; one database call.

SELECT "post"."id" AS "post_id", "post"."name" AS "post_name", "tag"."id" AS "tag_id", "tag"."name" AS "tag_name" 
FROM "post" "post"
LEFT JOIN "post_tags_tag" "post_tag"
ON "post_tag"."postId"="post"."id"
LEFT JOIN "tag" "tag"
ON "tag"."id"="post_tag"."tagId"
WHERE "post"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
-- PARAMETERS: [12,13,14,15,16,17,18,19,20,21]

Wrap Up

Hope you found this useful.

✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in codeburst

Bursts of code to power through your day. Web Development articles, tutorials, and news.

Written by John Tucker

Broad infrastructure, development, and soft-skill background

Responses (3)

Write a response