Dataloader Guidelines
Dataloader Guidelines¶
Introduction¶
This document offers a detailed explanation of the loadOne, loadMany, and BatchGrouping dataloader utilities in our FireFly service. These tools are designed to optimize data fetching in GraphQL or similar environments, enhancing performance and code readability.
Understanding DataLoader Utilities¶
loadOne: A tool for fetching single data items, especially when the underlying service does not support batch operations.loadMany: Ideal for batching multiple data fetches, to be used when the underlying service can handle multiple data items simultaneously.BatchGrouping: Manages complex batch operations, grouping data requests for efficient processing.
Quick Guide to LoadOneProps Interface in DataLoader¶
The LoadOneProps interface is essential in our decorator implementation. It plays a crucial role in both fetching data and configuring caching efficiently.
Structure of LoadOneProps¶
export interface LoadOneProps<Input> extends DaoProps {
input: Input;
}
input: The key parameter needed for fetching data.Extends
DaoProps: Inherits additional properties, typically including context.
Importance in DataLoader¶
Data Fetching:
LoadOnePropsprovides necessary inputs to the DataLoader functions (loadOne,loadMany).Caching: Essential in cache configuration;
LoadOnePropsallows access tocontextandinput, enabling DataLoader to create effective cache keys.
Example Usage:
@loadOne<YourInputType, YourReturnType>({
name: 'YourServiceName',
cacheIds: ['context.customerId', 'input.itemId'],
})
@loadMany<YourInputType, YourReturnType>({
name: 'YourServiceName',
cacheIds: ['context.customerId', 'input.itemId'],
})
Best Practices¶
Maintain a consistent structure in
input.Utilize
contextandinputfromLoadOnePropsfor efficient caching.
Singleton Nature of Data Returned by DataLoader¶
When using DataLoader in our application, it’s important to recognize that the data returned by a DataLoader instance often behaves like a singleton. This characteristic has crucial implications for handling the returned data:
Understanding Singleton Data Behavior¶
Shared Data Instances: Data fetched by a DataLoader is cached and returned as the same instance in subsequent requests. This means that if you fetch data and then modify it, the modifications will be reflected in all places where this data is used.
Global Impact of Modifications: Because the same data instance is shared across different parts of the application, any alterations to this data can have unintended global effects. Modifying the data directly can lead to inconsistencies and hard-to-track bugs.
Best Practices for Managing Data¶
Avoid Direct Modification: Treat the data returned by DataLoader as immutable. Rather than modifying this data directly, consider transforming it into a new object if modifications are necessary.
Deep Copies: If you need to alter the data, make a deep copy first. This ensures that the original cached data remains unchanged.
Awareness in Development: Be conscious of how data is shared and used across the application. Ensure that team members are aware of the implications of modifying DataLoader-fetched data.
Implications for Application Architecture¶
Consistent Data: The singleton nature of DataLoader data ensures consistency across fetches, which can be beneficial for performance and cache management.
Caution in Data Handling: Developers must exercise caution and clarity in handling the data, particularly when dealing with complex objects or nested data structures.
Using loadOne¶
Purpose: To fetch single data items when batch fetching isn’t supported by the underlying service. Example Usage:
class AmazonMusicCatalog {
@loadOne<GetTrackProps, Track>({
name: 'AmazonMusicCatalog.getTrack',
cacheIds: ['context.customerId', 'input.id', 'input.marketplace'],
})
async getTrack({ context, input }: LoadOneProps<GetTrackProps>): Promise<Track> {
// Implementation...
}
}
Key Points:
Use when underlying services or APIs do not support fetching multiple items at once.
Ensures efficient fetching and caching for individual items.
Using loadMany¶
Purpose: Batches and fetches multiple data items in a single operation, leveraging underlying services that support batch fetching. Example Usage:
class AmazonMusicCatalog {
@loadMany<GetTrackProps, Track>({
name: 'AmazonMusicCatalog.getTracks',
cacheIds: ['context.customerId', 'input.id', 'input.marketplace'],
})
async getTracks({ context, inputs }: LoadManyProps<GetTrackProps>): Promise<(Track | Error)[]> {
// Implementation...
}
}
Key Points:
Ideal for services/APIs that support retrieving multiple data items in a single request.
Enhances performance by reducing the number of calls to the data source.
To disable caching and only do batching for loadMany, we can omit the cacheIds and cacheKeyFn on the loadMany options. This can be understood as client would only want the batching but not the caching mechanism.
The BatchGrouping Class¶
Purpose: For complex batch operations where data needs grouping prior to processing. Example Usage:
class AmazonMusicCatalog {
@loadMany<GetTrackProps, Track>({
name: 'AmazonMusicCatalog.getTracks',
cacheIds: ['context.customerId', 'input.id', 'input.marketplace'],
})
async getTracks(props: LoadManyProps<GetTrackProps>): Promise<(Track | Error)[]> {
return `new`` ``BatchGrouping``<``string``,`` ``GetTrackProps``,`` ``Track``>()
``.``props``(props``)
``.``groupBy``(({ input }: LoadOneProps<GetTrackProps>) => input.marketplace``)
``.``batchFunction``({ context, inputs } => {
// inputs here will only contains input that has the same marketplace
}``)
``.``execute``();`
}
}
Key Points:
Facilitates efficient data processing in sophisticated batch scenarios.
Enable cloning¶
FireFly dataloader supports cloning data before. If you want to enable this to help ease the migration to singleton concept, you can enable it like so
class AmazonMusicCatalog {
@loadMany<GetTrackProps, Track>({
name: 'AmazonMusicCatalog.getTracks',
enableCloning: true
cacheIds: ['context.customerId', 'input.id', 'input.marketplace'],
})
async getTracks(props: LoadManyProps<GetTrackProps>): Promise<(Track | Error)[]> {
//...
}
@loadOne<GetTrackProps, Track>({
name: 'AmazonMusicCatalog.getTrack',
enableCloning: true
cacheIds: ['context.customerId', 'input.id', 'input.marketplace'],
})
async getTrack(props: LoadOneProps<GetTrackProps>): Promise<(Track | Error)[]> {
//...
}
}
Track in FireFly is special because different context like standalone track, album, playlist may have different eligibility for track so we may want separate object for each case.
Conclusion¶
The choice between loadOne and loadMany should be guided by the capabilities of your underlying data services. BatchGrouping adds an extra layer of efficiency for complex data fetching scenarios. These DataLoader utilities are designed to improve our application’s data handling efficiency and performance.
FAQs¶
Q: When should I use loadMany instead of loadOne? A: Use loadMany when the underlying service supports batch fetching of data. If the service can only handle individual requests, use loadOne.
