【问题标题】:Using dataloader for resolvers with nested data from ArangoDB使用来自 ArangoDB 的嵌套数据的解析器的数据加载器
【发布时间】:2018-11-14 12:10:33
【问题描述】:

我正在 ArangoDB(使用 arangojs)上实现一个 GraphQL API,我想知道如何为这个非常基本的用例最好地实现 dataloader(或类似的)。

我有 2 个带有如下所示数据库查询的解析器(这两个都有效),第一个获取 Person,第二个获取与给定 Person 关联的 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)),
    }

}

现在,如果我想在单个请求中获取带有 Records 的 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 以使用批处理请求/缓存?我可以以某种方式在fetchPerson(id) 内运行fetchRecords(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
}
`

【问题讨论】:

    标签: javascript graphql arangodb apollo-server arangojs


    【解决方案1】:

    因此,dataloader 的真正好处在于它可以阻止您进行 n+1 次查询。例如,如果在您的架构中,Person 有一个字段记录,然后您要求前 10 个人的 10 条记录。在一个简单的 gql 模式中,这将导致 11 个请求被触发:前 10 个人有 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),
    };
    

    然后您想将加载程序附加到您的context 以便于共享。来自 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 对象中的 records 数据。
    • 请注意,每个查询共享相同的输入:用户 ID。如果我只需要像firstName 这样的person 属性,那么查询与person 关联的所有records 对象是没有意义的。但是,如果查询在person 下包含records,则查询该数据并以嵌套结构返回它是有意义的。这是我问题的症结所在,我该怎么做?
    【解决方案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 提供的。

    【讨论】:

      猜你喜欢
      • 2019-04-08
      • 1970-01-01
      • 2020-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多