I'll continue my love affair with Graphql this week by digging into an issue that caused a lot of pain when we switched from Appsync serverless Graphql APIs to running them on Nest with Apollo (more on that later).
After switching to a graphql service that was hosted in a single managed process (AWS Lambda in our case), we started to see big performance degradation when doing nested queries. These bottlenecks existed all along, but didn't become apparent when Appsync was managing our compute directly via Lambdas, when we started managing the backend ourselves, we hit N+1 issues that caused queries to drop from 2 sec to 30 seconds overnight. Here's how we fixed it and how we measured the impact of our changes.
The N+1 Problem
Let's get our definitions straight before we dive in. N+1 problem is something that can happen in GraphQl when your schema relies on nested resolvers. Let's look at an example
type Student {
id: ID!
name: String!
registrations: [Registration]
}
type Registration{
id: ID!
courseName: String
teachers: [Teacher]
}
type Teacher {
id: ID!
name: String!
department: Department
}
type Department{
id: ID!
name:String!
}
type query{
getStudents: [Student]
}
So in this simple example we have a query to get a list of students, but for each student, we can also fetch the classes they are in, and for each class the professor(s) and which department they are in. Works fine with a small number of students, but let's assume we're doing this for a university with 1000s of students. Without optimization, a single query for 100 students, could trigger 1000s of additional calls to resolvers to fetch Teachers and and Departments and many of those Teacher and Department calls would be duplicates, so the query would be expensive and poorly optimized.
Enter Data loaders
Using a data loader pattern in this case would allow you to defer the loading of nested objects until the later in the query and only fetch unique ids as a single batched request which could improve performance in this case many fold.
Using NestJS as our platform (which uses Apollo under the hood) we can wire them up like so.
First: Define a data loader object for your type
// teacher data loader
@Injectable()
export class TeacherDataLoader {
constructor(private readonly teacherRepository: TeacherRepository) {}
public createLoaderBySelf(): DataLoader<string, TeacherModel> {
return new DataLoader<string, TeacherModel>(
async (ids: readonly string[]) => {
const result = await this.teacherRepository.getByIdsDataloader(ids);
const result = mapTeachers(ids, result);
return result;
}
);
}
}
export const mapTeachers = (
ids: readonly string[],
teachers: TeacherModel[]
): TeacherModel[] => {
const result = ids.map((id) => teachers.find((x) => x.id === id) || null);
return result;
};
What this loader is doing is accepting a list of ids, fetching all the teachers in a single call to our repository and then mapping them by id back to our resolver.
Next:Add it to your graphql context
It can be accessed in the Nest resolver like this:
export class GraphqlContext {
constructor(
private readonly teacherDataLoader: TeacherDataLoader){}
async getContext(props: GetContextProps): Promise<IGraphqlContext> {
const context: IGraphqlContext = {}
context.teacherDataLoader = teacherDataLoader;
}
}
Then: Use it in a resolver
And in the file responsible for resolving Students you refer to your loader which is available in the graphql context
@Resolver('Teacher')
export class TeacherResolver {
constructor() {}
@ResolveReference()
async resolveReference(
reference: {
__typename: string;
id: string;
},
context: IGraphqlContext
) {
const { id } = reference;
const teacher = await context.teacherDataLoader.createLoaderBySelf.load(id);
return teacher;
}
}
This example is using schema first development from NestJS. Here is the docs on the nest homepage. Happy coding!
Top comments (0)