简体   繁体   中英

Using dataloader for resolvers with nested data from ArangoDB

I'm implementing a GraphQL API over ArangoDB (with arangojs) and I want to know how to best implement dataloader (or similar) for this very basic use case.

I have 2 resolvers with DB queries shown below (both of these work), the first fetches Persons, the 2nd fetches a list of Record objects associated with a given Person (one to many). The association is made using ArangoDB's edge collections.

 import { Database, aql } from 'arangojs' import pick from 'lodash/pick' const db = new Database('http://127.0.0.1:8529') db.useBasicAuth('root', '') db.useDatabase('_system') // id is the auto-generated userId, which `_key` in Arango const fetchPerson = id=> async (resolve, reject)=> { try { const cursor = await db.query(aql`RETURN DOCUMENT("PersonTable", ${String(id)})`) // Unwrap the results from the cursor object const result = await cursor.next() return resolve( pick(result, ['_key', 'firstName', 'lastName']) ) } catch (err) { return reject( err ) } } // id is the auto-generated userId (`_key` in Arango) who is associated with the records via the Person_HasMany_Records edge collection const fetchRecords = id=> async (resolve, reject)=> { try { const edgeCollection = await db.collection('Person_HasMany_Records') // Query simply says: `get all connected nodes 1 step outward from origin node, in edgeCollection` const cursor = await db.query(aql` FOR record IN 1..1 OUTBOUND DOCUMENT("PersonTable", ${String(id)}) ${edgeCollection} RETURN record`) return resolve( cursor.map(each=> pick(each, ['_key', 'intro', 'title', 'misc'])) ) } catch (err) { return reject( err ) } } export default { Query: { getPerson: (_, { id })=> new Promise(fetchPerson(id)), getRecords: (_, { ownerId })=> new Promise(fetchRecords(ownerId)), } } 

Now, if I want to fetch the Person data with the Records as nested data, in a single request, the query would be this:

 aql` LET person = DOCUMENT("PersonTable", ${String(id)}) LET records = ( FOR record IN 1..1 OUTBOUND person ${edgeCollection} RETURN record ) RETURN MERGE(person, { records: records })` 

So how should I update my API to employ batch requests / caching? Can I somehow run fetchRecords(id) inside of fetchPerson(id) but only when fetchPerson(id) is invoked with the records property included?

The setup file here, notice I'm using graphql-tools , because I took this from a tutorial somewhere.

 import http from 'http' import db from './database' import schema from './schema' import resolvers from './resolvers' import express from 'express' import bodyParser from 'body-parser' import { graphqlExpress, graphiqlExpress } from 'apollo-server-express' import { makeExecutableSchema } from 'graphql-tools' const app = express() // bodyParser is needed just for POST. app.use('/graphql', bodyParser.json(), graphqlExpress({ schema: makeExecutableSchema({ typeDefs: schema, resolvers }) })) app.get('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })) // if you want GraphiQL enabled app.listen(3000) 

And here's the schema.

 export default ` type Person { _key: String! firstName: String! lastName: String! } type Records { _key: String! intro: String! title: String! misc: String! } type Query { getPerson(id: Int!): Person getRecords(ownerId: Int!): [Record]! } type Schema { query: Query } ` 

So, the real benefit of dataloader is that it stops you from doing n+1 queries. Meaning for example, if in your schema, Person had a field records, and then you asked for the first 10 people's 10 records. In a naive gql schema, that would cause 11 requests to be fired: 1 for the first 10 people, and then one for each of their records.

With dataloader implemented, you cut that down to two requests: one for the first 10 people, and then one for all of the records of the first ten people.

With your schema above, it doesn't seem that you can benefit in any way from dataloader, since there's no possibility of n+1 queries. The only benefit you might get is caching if you make multiple requests for the same person or records within a single request (which again, isn't possible based on your schema design unless you are using batched queries).

Let's say you want the caching though. Then you could do something like this:

// loaders.js
// The callback functions take a list of keys and return a list of values to
// hydrate those keys, in order, with `null` for any value that cannot be hydrated
export default {
  personLoader: new DataLoader(loadBatchedPersons),
  personRecordsLoader: new DataLoader(loadBatchedPersonRecords),
};

You then want to attach the loaders to your context for easy sharing. Modified example from Apollo docs:

// app.js
import loaders from './loaders';
app.use(
  '/graphql',
  bodyParser.json(),
  graphqlExpress(req => {
    return {
      schema: myGraphQLSchema,
      context: {
        loaders,
      },
    };
  }),
);

Then, you can use them from the context in your resolvers:

// ViewerType.js:
// Some parent type, such as `viewer` often
{
  person: {
    type: PersonType,
    resolve: async (viewer, args, context, info) => context.loaders.personLoader,
  },
  records: {
    type: new GraphQLList(RecordType), // This could also be a connection
    resolve: async (viewer, args, context, info) => context.loaders.personRecordsLoader;
  },
}

I guess I was confused about the capability of dataloader. Serving nested data was really the stumbling block for me.

This is the missing code. The export from resolvers.js needed a person property,

export default {

    Person: {
        records: (person)=> new Promise(fetchRecords(person._key)),
    },
    Query: {
        getPerson: (_, { id })=> new Promise(fetchPerson(id)),
        getRecords: (_, { ownerId })=> new Promise(fetchRecords(ownerId)),
    },

}

And the Person type in the schema needed a records property.

type Person {
    _key: String!
    firstName: String!
    lastName: String!
    records: [Records]!
}

Seems these features are provided by Apollo graphql-tools .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM