Использование загрузчика данных для распознавателей с вложенными данными из ArangoDB
Я реализую GraphQL API через ArangoDB (с arangojs), и я хочу знать, как лучше всего реализовать dataloader
(или аналогичный) для этого очень простого варианта использования.
У меня есть 2 резольвера с запросами к БД, показанными ниже (оба из них работают), первый выбирает Persons, второй выбирает список объектов Record, связанных с данным Person (один ко многим). Ассоциация создана с использованием краевых коллекций 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)),
}
}
Теперь, если я хочу получить данные Person с записями как вложенные данные, в одном запросе, запрос будет следующим:
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
собственность включена?
Файл установки здесь, обратите внимание, что я использую graphql-tools
потому что я взял это из учебника где-то.
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
}
`
2 ответа
Я думаю, что я был смущен возможностью загрузчика данных. Обслуживание вложенных данных было для меня камнем преткновения.
Это недостающий код. Экспорт из 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
,
Итак, реальное преимущество dataloader в том, что он мешает вам выполнить n+1 запросов. Это означает, например, что если в вашей схеме Person есть поле записей, а затем вы запросили первые 10 записей 10 человек. В наивной схеме gql это вызовет 11 запросов: 1 для первых 10 человек, а затем по одному для каждой из их записей.
Внедрив dataloader, вы сократите это до двух запросов: один для первых 10 человек, а затем один для всех записей первых десяти человек.
С вашей схемой выше, кажется, что вы не можете извлечь какую-либо выгоду из dataloader, так как нет возможности 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;
},
}