/ graphql

GraphQL Query Batching Solutions

I've been using Apollo for a few months now and the one problem not well documented is query batching. Most of the getting started guides don't even mention it. This is a shame considering almost all apps would need to fetch data from some sort of database. These principles can be taken to other GraphQL implementations as well, but the code shown is specific to node in this tutorial.

Two Query Minimum: Dataloader

The first solution is Facebook's Dataloader. I'm a little surprised at how complicated the documentation makes this library seem, when it's actually simple to use. I define a dataloader in the same place as my model for each database table:

import DataLoader from 'dataloader';

export const categoryLoader = new DataLoader(async ids => {
  let categories = await Category
    .query()
    .whereIn('id', ids);

  return ids
    .map(
      id => categories.find(
        category => category.id === id
      )
    );
});

The second part (mapping through the result and reordering) is only needed because the database ORM I use does not return whereIn's in the same order as I pass them. Now let's look at the resolver:

Receipt: {
  async category(receipt) {
    return categoryLoader.load(receipt.category_id);
  },
}

The query looks like this:

query Receipts(limit: 50) {
  id
  name
  category {
    name
  }
}

If we loaded a list of 50 without the dataloader, we would be querying once for all receipts, and then 50 times for each category. Since we are using the dataloader library, we will be making two queries. One big whereIn with all categories at once, and the main receipts query.

I should mention, if you have other relationships, you will have a query for each of them. Ex: user has a company, category, and membership, you'd have 4 queries even with a dataloader.

This is probably fine, and will be performant enough. Personally, I still find this frustrating. When I work with REST apis, I don't have this problem. I simply write a query that joins on the category, which results in a single query to the database.

We can do this with GraphQL, but it feels like you are doing somthing you shouldn't be. I'm going to share how I've made this work and would love to see what people on the Apollo / GraphQL team think. I'd like this to be easier to do.

A Single Query

The GraphQL spec passes an info object with a list of fields queried for that you can get access to. This will let us see what relationships are queried for in a parent resolver. There's no way to get around doing this, because the parent resolver must return before the children resolvers can be ran, so we have to do all the work in the parent.

const EagerQL = (info, allowed) => {
  let fields =
    info.operation
      .selectionSet.selections[0]
      .selectionSet.selections;

  return fields
    .map(field => field.name.value)
    .filter(field => allowed.includes(field));
};

Since all fields, even ones on the Receipt itself, will be returned from the info object, the second parameter is a list of valid relationships that the function will return. This way we can now do a single query and join on any relationships!

Query: {
  receipts(_, args, { user }, info) {
    let eagers = EagerQL(info, ['company', 'category'])

    return Company
      .query()
      .findById(user.company_id)
      .with(eagers);
  },
},

If the query looks like the one we mentioned earlier:

query Receipts(limit: 50) {
  id
  name
  category {
    name
  }
}

The eagers variable would be an array with just category: ['category'].

Conclusion

I wish you didn't have to reach deep inside the GraphQL info object to find out what fields are queried for. The one problem with GraphQL and child resolvers waiting to be passed the parent result is that it isn't trivial to join queries. I understand why this is the case, and it makes GraphQL a joy to use in every way, except for this one issue. I'd love to hear feedback @zachcodes if anyone else has struggled with querying directly in resolvers!