One of the common problems that most of the developers face with graphql, is the N + 1 problem, it means that the server executes multiple unnecessary round trips to datastores for nested data, resulting in slow response times and expensive requests
For Example
graphql
query{
users{
id,
full_name,
pets{
name
}
}
}
in the query above, will be executed 1 query to the database to the datastore get the users and for each user another extra query for each user to fetch its pets (N queries for N users) resulting in N + 1.
Now What to do?
to solve this problem the Graphql team in Facebook created Dataloader
wich batch our data, AKA: request all the data from the database, at the start of the server, and when we are going to request one item, all that we do is pass down the identifier of the item and execute a search inside the array of data, which is a lot faster than execute a query to the database every time is made a request because is retrieving data from memory not datastore
Once this is clarified let's puts our hands on
for this post I'll be using the API example I use for my last post after clone into your PC the repo and install everything
run:
bash
yarn add dataloader
</code>
create the following folder and file
...
src/
utils/
loaders.ts
...
and in uutils/loaders.ts
import {BatchLoadFn} from 'dataloader';
import {Pet,User} from '../database/models';
export const Pets : BatchLoadFn<number,Array<Pet>>= async (ids)=>{
const pets= await Pet.query();
return ids.map(i=>pets.filter(item=> item.id===i))
};
export const Users : BatchLoadFn<number,Array<User>> = async (ids)=>{
const users = await User.query();
return ids.map(id=> users.filter(i=> i.id===id));
}
the way it will be used in our resolvers is through the context :
src/index.ts
typescript
...
import DataLoader from 'dataloader';
import {Pets,Users} from './utils/loaders';
...
...
const config : Config = {
schema:schema,
introspection: true,//these lines are required to use the gui
playground: true,// of playground
context:{
loaders:{
users: new DataLoader(Users),
pets: new DataLoader(Pets),
}
}
}
...
now in your resolvers instead of using a query to get the data nested
you use the loaders passed down through the context and using the id to find the match in the loaders
import {Pet,User} from '../../database/models'; | |
import {Resolvers} from '../../__generated__/generated-types'; | |
import {UserInputError} from 'apollo-server-express'; | |
const resolvers : Resolvers = { | |
Query:{ | |
pet:async (parent,args,ctx)=>{ | |
const pet:Pet= await Pet.query().findById(args.id); | |
return pet; | |
}, | |
pets: async (parent,args,ctx)=>{ | |
const pets:Pet[]= await Pet.query(); | |
return pets; | |
} | |
}, | |
Pet:{ | |
owner:async(parent,args,ctx)=>{ | |
const {loaders:{users}} = ctx; | |
return users.load(parent.id); | |
} | |
}, | |
Mutation:{ | |
createPet:async (parent,args,ctx)=>{ | |
let pet: Pet; | |
try { | |
pet = await Pet.query().insert({...args.pet}); | |
} catch (error) { | |
throw new UserInputError("Bad user input fields required",{ | |
invalidArgs: Object.keys(args), | |
}); | |
} | |
return pet; | |
}, | |
updatePet:async (parent,{pet:{id,...data}},ctx)=>{ | |
const pet : Pet = await Pet.query() | |
.patchAndFetchById(id,data); | |
return pet; | |
}, | |
deletePet:async (parent,args,ctx)=>{ | |
const pet = await Pet.query().deleteById(args.id); | |
return "Successfully deleted" | |
}, | |
} | |
} | |
export default resolvers; |
import { Resolvers} from '../../__generated__/generated-types'; | |
import {User,Pet} from '../../database/models'; | |
import {UserInputError} from 'apollo-server-express'; | |
interface assertion { | |
[key: string]:string | number ; | |
} | |
type StringIndexed<T> = T & assertion; | |
const resolvers : Resolvers ={ | |
Query:{ | |
users: async (parent,args,ctx)=>{ | |
const users : User[] = await User.query(); | |
return users; | |
}, | |
user:async (parent,args,ctx)=>{ | |
const user :User = await await User.query().findById(args.id); | |
return user; | |
}, | |
}, | |
User:{ | |
pets:async (parent,args,ctx)=>{ | |
const {loaders:{pets}} = ctx; | |
return pets.load(parent.id); | |
} | |
}, | |
Mutation:{ | |
createUser:async (parent,args,ctx)=>{ | |
let user : User; | |
try { | |
user = await User.query().insert({...args.user}); | |
} catch (error) { | |
console.log(error); | |
throw new UserInputError('Email Invalido', { | |
invalidArgs: Object.keys(args), | |
}); | |
} | |
return user; | |
}, | |
updateUser:async (parent,{user:{id,...data}},ctx)=>{ | |
let user : User = await User.query().patchAndFetchById(id,data); | |
return user; | |
}, | |
deleteUser:async (parent,args,ctx)=>{ | |
const deleted = await User.query().deleteById(args.id); | |
return "Succesfull deleted"; | |
}, | |
} | |
} | |
export default resolvers; |
And we can appreciate an increase of performance in the response
Thank's for read it , if you have any suggestion for me I would love to know it, leave it below in the comments box, if you liked it , please follow me here and also on twitter, Thanks! take care!
Top comments (0)