Использование загрузчика данных для распознавателей с вложенными данными из ArangoDB

Я реализую GraphQL API поверх ArangoDB (с arangojs) и хочу знать, как лучше всего реализовать dataloader (или аналогичный) для этого очень простого варианта использования.

У меня есть 2 преобразователя с запросами к БД, показанными ниже (оба из них работают), первый извлекает людей, 2-й извлекает список объектов Record, связанных с данным человеком (от одного ко многим). Ассоциация выполняется с использованием краевых коллекций ArangoDB.

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)),
    }

}

Теперь, если я хочу получить данные о человеке с записями как вложенные данные в одном запросе, запрос будет таким:

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

Итак, как мне обновить свой API, чтобы использовать пакетные запросы/кэширование? Могу ли я как-то запустить fetchRecords(id) внутри fetchPerson(id), но только когда fetchPerson(id) вызывается с включенным свойством records?

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)

И вот схема.

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
}
`


person rm.rf.etc    schedule 05.06.2018    source источник


Ответы (2)


Итак, реальное преимущество загрузчика данных заключается в том, что он не позволяет вам выполнять n+1 запросов. Это означает, например, что если в вашей схеме у человека есть записи поля, а затем вы запросили 10 записей первых 10 человек. В наивной схеме gql это приведет к запуску 11 запросов: 1 для первых 10 человек, а затем по одному для каждой из их записей.

С реализованным загрузчиком данных вы сократите это до двух запросов: один для первых 10 человек, а затем один для всех записей первых десяти человек.

С вашей схемой выше не похоже, что вы можете получить какую-либо выгоду от загрузчика данных, поскольку нет возможности n + 1 запросов. Единственное преимущество, которое вы можете получить, — это кэширование, если вы делаете несколько запросов для одного и того же человека или записей в одном запросе (что опять же невозможно на основе вашей схемы, если вы не используете пакетные запросы).

Допустим, вам нужно кэширование. Тогда вы можете сделать что-то вроде этого:

// 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),
};

Затем вы хотите подключить загрузчики к вашему контексту. для удобного обмена. Модифицированный пример из документов Apollo:

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

Затем вы можете использовать их из контекста в ваших преобразователях:

// 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;
  },
}
person Matthew Herbst    schedule 11.06.2018
comment
Я все еще потерян. Я не понимаю, как вернуть данные records, вложенные в объект person. - person rm.rf.etc; 15.06.2018
comment
Обратите внимание, что каждый запрос имеет одни и те же входные данные: идентификатор пользователя. Нет смысла запрашивать все records объекты, связанные с person, если мне нужны только person атрибуты, такие как firstName. Однако если запрос включает records под person, то имеет смысл запросить эти данные и вернуть их во вложенной структуре. Это суть моего вопроса, как мне это сделать? - person rm.rf.etc; 15.06.2018

Наверное, меня смутили возможности загрузчика данных. Обслуживание вложенных данных было для меня настоящим камнем преткновения.

Это недостающий код. Для экспорта из resolvers.js требовалось свойство person,

export default {

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

}

А для типа Person в схеме требовалось свойство records.

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

Похоже, эти функции предоставляет Apollo graphql-tools.

person rm.rf.etc    schedule 15.06.2018