- What is BaseLoader?
- What is DataLoader?
- How does BaseLoader work?
- How to use BaseLoader?
- loadOne
- Example
- loadByRelatedId
- Example
- Page Size
What is BaseLoader?
BaseLoader, an extension of DataLoader is included in the Graphweaver package, is a powerful tool that enhances data loading capabilities in GraphQL APIs. It builds upon DataLoader, an open-source library developed by Facebook, to provide batching, caching, and performance optimisation features.
What is DataLoader?
Before diving into BaseLoader, let's understand DataLoader.
DataLoader is a utility library designed to handle data batching and caching. It allows you to fetch multiple individual items from a data source, such as a database, in a single request instead of making separate requests for each item.
Imagine you have a Task entity that has a many-to-one relationship with a User entity, and you want to retrieve a list of users as well as a list of each user’s tasks. With DataLoader, you can batch all the requests together and fetch all the task details in a single query, significantly improving efficiency.
DataLoader also caches the results of each request, reducing the need for redundant queries in the future.
DataLoader is particularly valuable in GraphQL servers where you often need to fetch related data.
Now, let's explore how BaseLoader leverages DataLoader to enhance data loading capabilities in Graphweaver.
How does BaseLoader work?
BaseLoader provides an interface for loading single and related entities with caching, error handling, and performance optimisation features. It uses DataLoader under the hood to batch and optimise data retrieval.
BaseLoader consists of two primary functions:
loadOne
: Loads a single entity by ID using DataLoader.loadByRelatedId
: Loads related entities based on a related ID and a filter using DataLoader.
These functions enable efficient data loading by batching requests and reducing unnecessary trips to the data source.
How to use BaseLoader?
Below are examples for using loadOne
and loadByRelatedId
.
For the sake of explanation, we’re going to use BaseLoader in these examples ourselves instead of @RelationshipField
decorators.
@RelationshipField
decorator uses the BaseLoader functions underneath the hood. For more on this decorator, see the decorators section.loadOne
loadOne
loads a single entity of a given type and ID, as its name suggests.
Example
In this example, we add a custom field to a GraphQL entity, using Baseloaders.loadOne
to retrieve a related entity.
/*
This little example snippet is based on an example in the Graphweaver repository on Github
https://github.com/exogee-technology/graphweaver/tree/main/src/examples/databases
*/
@Entity('Task', {
provider: new MikroBackendProvider(OrmTask, myConnection),
})
export class Task {
...
@Field(() => User)
async user(task: Task) {
const taskDataEntity = dataEntityForGraphQLEntity(
task as unknown as WithDataEntity<OrmTask>
);
const user = await BaseLoaders.loadOne({
gqlEntityType: Task,
id: taskDataEntity.userId,
});
return fromBackendEntity(User, user);
}
...
}
dataEntityForGraphQLEntity
lets us get the underlying entity that models the ‘Task’ - refer to the documentation on data entities for more.
In this example snippet, we’re
- Retrieving the data entity under our GraphQL entity
- Using the task data entity’s property
userId
to load the related user - Returning the user as a GraphQL entity with
fromBackendEntity
.
fromBackendEntity
ensures the client can access nested fields on this entity.
BaseLoader handles a lot of the magic behind the scenes.
This is extremely important if you are using custom field resolvers. Custom field resolvers will be called many times as the graph is resolved. When using BaseLoader, you are ensuring this is as performant as possible.
loadByRelatedId
loadByRelatedId
loads a collection of entities, rather than one. You can think of it as the ‘inverse’ of loadOne
.
Example
Imagine you wanted to do the inverse of the loadOne
example above, and add a custom field to the User
GraphQL entity for the user’s tasks.
The data entity for our user has no record of its own tasks, or their IDs. But, as before, the task data entities have a field called userId
. Using this related ID, we can load the list of tasks for a given user.
@Entity<User>('User', {
provider: new MikroBackendProvider(OrmUser, pgConnection),
})
export class User {
@Field(() => ID)
id!: string;
...
@Field(() => [Task])
async tasks(user: User) {
const tasks = await BaseLoaders.loadByRelatedId({
gqlEntityType: Task,
relatedField: 'userId',
id: user.id,
});
return tasks.map((task) => fromBackendEntity(Task, task));
}
...
}
Using the user’s own ID, we load the related tasks. The user’s ID will be reflected on the tasks under the userId
property, so we specify that as the related field.
Then, as before, we then wrap them with fromBackendEntity
before returning them so that clients can traverse the graph from these entities.
Page Size
Some backend providers do not let you fetch unlimited amounts of items at once, either because the query becomes too large so they reject it, or because the performance when asking for large pages decreases dramatically. In these scenarios you can configure the page size for the loader by creating your adapter. It can use its own data loader with the maxBatchSize
parameter configured for this data source instead of leveraging BaseLoaders
.
Examples are available in our Graphweaver Enterprise adapters package, so if you need more guidance here, just reach out.