graphql / dataloader

DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching.
MIT License
12.84k stars 510 forks source link

[QUESTION] Is possible chain two dataloaders? #229

Open sneznaovca opened 4 years ago

sneznaovca commented 4 years ago

Hi, is it possible chain two dataloaders? I have one resolver where I need using two dataloaders, is it possible? I can't use SQL join because it's slow.

async (row, _, { dataSources }) => {
    const partialData = await dataloader1(dataSources).load(row.id);
    if (partialData === null) {
        return null;
    }
    return await dataloader2(dataSources).load(partialData.id);
}
klausXR commented 4 years ago

A couple of weeks ago I was experimenting with something and I needed to do something similar to this, I came up with this solution

class MyDataLoader extends DataLoader {
    constructor(batchLoadFn, options) {
        // Initialize the DataLoader class
        super(batchLoadFn, options)

        // A custom function to run before .load resolves.
        // It doesn't do anything by default, it just returns what 
        // was passed to it
        this._beforeResolveFn = row => row
    }

    // Overwrite the default load handler, so that we have a chance
    // to run our custom method
    // .loadMany is just a wrapper around .load, which is why we only need to
    // overwrite this one.
    async load(key) {
        // Call the original load method, passing in the key,
        // then run our middleware function, and after it resolves,
        // return the result to the caller.

        // You can imagine that we are trying to resolve a Many to Many relationship,
        // where the result of calling super.load returns an array of foreign keys.
        // We can then pipe them through another dataloader to get the actual objects
        return super.load(key)
           .then(this._beforeResolveFn)
    }

    // Sets a custom function to run before the .load promise resolves
    async beforeResolve(fn) {
        this._beforeResolveFn = fn
    }
}

// Arbitrary usage
const ProductLoader = new MyDataLoader(...) // accepts productId
const CategoryLoader = new MyDataLoader(...) // accepts categoryId

// accepts productId, returns a list of categoryId's, like
// `SELECT * FROM productsCategories pc WHERE pc."productId" IN (...)` 
const ProductCategoryLoader = new MyDataLoader() 

// We know that the ProductCategoryLoader returns us an array of ids,
// So we pipe its result through the CategoryLoader, and after that resolves
// .load will return an array of categories to the ProductCategoryLoader.load callee
ProductCategoryLoader.beforeResolve(categoryIds => CategoryLoader.loadMany(categoryIds))

ProductCategoryLoader.load(1) // Product id
  .then(categories => {...} ) // An array of categories 

I figured it would somewhat simplify the more complex use-cases.

It requires extending the base class, but its a transparent wrapper by default. Also, I just wrote this in a text editor, didn't test this specific implementation, because I don't have access to my code at the moment, but the general idea is the same ( in case it doesn't work after copy/paste ).