Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

handling GraphQL field arguments using Dataloader?

I'm wondering if there's any consensus out there with regard to how best to handle GraphQL field arguments when using Dataloader. The batchFn batch function that Dataloader needs expects to receive Array<key> and returns an Array<Promise>, and usually one would just call load( parent.id ) where parent is the first parameter of the resolver for a given field. In most cases, this is fine, but what if you need to provide arguments to a nested field?

For example, say I have a SQL database with tables for Users, Books, and a relationship table called BooksRead that represent a 1:many relationship between Users:Books.

I might run the following query to see, for all users, what books they have read:

query {
  users {
    id
    first_name
    books_read {
      title
      author {
        name
      }
      year_published
    }
  }
}

Let's say that there's a BooksReadLoader available within the context, such that the resolver for books_read might look like this:

const UserResolvers = {
  books_read: async function getBooksRead( user, args, context ) {
    return await context.loaders.booksRead.load( user.id );
  }
};

The batch load function for the BooksReadLoader would make an async call to a data access layer method, which would run some SQL like:

SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?);

We would create some Book instances from the resulting rows, group by user_id, then return keys.map(fn) to make sure we assign the right books to each user_id key in the loader's cache.

Now suppose I add an argument to books_read, asking for all the books a user has read that were published before 1950:

query {
  users {
    id
    first_name
    books_read(published_before: 1950) {
      title
      author {
        name
      }
      year_published
    }
  }
}

In theory, we could run the same SQL statement, and handle the argument in the resolver:

const UserResolvers = {
  books_read: async function getBooksRead( user, args, context ) {
    const books_read = await context.loaders.booksRead.load( user.id );
    return books_read.filter( function ( book ) { 
      return book.year_published < args.published_before; 
    });
  }
};

But, this isn't ideal, because we're still fetching a potentially huge number of rows from the Books table, when maybe only a handful of rows actually satisfy the argument. Much better to execute this SQL statement instead:

SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?) AND B.year_published < ?;

My question is, does the cacheKeyFn option available via new DataLoader( batchFn[, options] ) allow the field's argument to be passed down to construct a dynamic SQL statement in the data access layer? I've reviewed https://github.com/graphql/dataloader/issues/75 but I'm still unclear if cacheKeyFn is the way to go. I'm using apollo-server-express. There is this other SO question: Passing down arguments using Facebook's DataLoader but it has no answers and I'm having a hard time finding other sources that get into this.

Thanks!

like image 486
diekunstderfuge Avatar asked May 23 '19 00:05

diekunstderfuge


1 Answers

Pass the id and params as a single object to the load function, something like this:

const UserResolvers = {
  books_read: async function getBooksRead( user, args, context ) {
    return context.loaders.booksRead.load({id: user.id, ...args});
  }
};

Then let the batch load function figure out how to satisfy it in an optimal way.

You'll also want to do some memoisation for the construction of the object, because otherwise dataloader's caching won't work properly (I think it works based on identity rather than deep equality).

like image 169
Andrew Ingram Avatar answered Sep 23 '22 08:09

Andrew Ingram