In Apollo GraphQL, how do I setup a resolver to not loop values?

1.4k views Asked by At

I'm using Sequelize in Node.js with Apollo-Server and Express.js.

When making queries that go deeper and deeper, GraphQL is looping my models and doing a separate query by ID on each of those.

For example, if I get user(userId) > playthroughs > scores, this will do a lookup for that user (no problem), then a lookup for all the playthroughs with that userId (still no a big deal), but then to get the scores, it loops each playthroughId and does a completely separate query on each. This is ridiculously inefficient and causes my queries to take way longer than they should.

Instead of looping:

SELECT scoreValue
FROM scores
WHERE playthroughId = id

I'd really like to grab the array myself and do that loop like this:

SELECT scoreValue
FROM scores
WHERE playthroughId IN (...ids)

This also happened when I used the reference GraphQL from Facebook last year, so I don't think it's specific to Apollo's implementation.

I'd like to know how I can tweak these queries so they're not taking such a performance hit.

Example resolvers:

const resolvers = {
    Query: {
        user: (_, values) => User.findOne(formatQuery(values))
            .then(getDataValues),
    },

    Playthrough: {
        score: ({ playthroughId }) => Score.findOne(formatQuery({ playthroughId }))
            .then(getDataValues),
    },

    User: {
        playthroughs: ({ userId }, { take }) => Playthrough.findAll(formatQuery({ userId, take, order: 'playthroughId DESC' }))
            .then(getAllDataValues),
    },
}
1

There are 1 answers

2
Jan Aagaard Meier On

In addition to graphql, facebook has also released a much lesser known project, dataloader.

What it does it batch several requests in the same tick into one. So your code would be something like

scoreLoader = new Dataloader(keys => {
  return Score.findAll({ where: { id: keys } }).then(() => {
     //Map the results back so they are in the same order as keys
  })
});


score: ({ playthroughId }) => scoreLoader.load(playthroughId).then(getDataValues)

Of course, having a load for each field is going to be tedious. So instead you can use dataloader-sequelize, which wrap all calls to association.get (i.e. Playthrough.getScores()) and calls to findOne / findById to dataloader calls, so several calls are batched in one.

Since you are building a graphql API backed by sequelize, you might also be interested in https://github.com/mickhansen/graphql-sequelize/, which provides sequelize specific helpers for grahpql, and uses dataloader-sequelize below the hood