在GraphQL参考实现中,解析器应返回Iterable,如何返回异步Iterable?

时间:2018-09-30 11:29:57

标签: node.js asynchronous graphql graphql-js

我正在使用Sequelize访问我的关系数据库并将结果传递到GraphQL解析器中。 Sequelize框架内的查询是异步执行的(bluebird)。为了缓冲大型结果集,并避免在服务器上出现高内存需求,例如请求了数百万条记录,我想到了在解析器中返回Iterator。考虑一下这个简化的要点:

// root resolver
function allPersons(...) {
  [...]
  return {
    nextId: 1,
    maxId: 10000000, 
    [Symbol.iterator]: () => { return this },
    next: function() {
      let nextRes = { done: true, value: null }
      if (this.nextId <= this.maxId) {
        nextRes.value = sequelize.models.person.findById(this.currId)
        nextRes.done = false
        this.nextId = this.nextId + 1
      }
      return nextRes
    }
}

以上方法之所以有效,是因为将Sequelize构造的Promise作为next()的{​​{1}}返回。解决此价值承诺后,它将从基础关系数据库中获取一条记录。因此,我同步构造了异步数据获取。这仅起作用,因为每个单独的提取都独立于所有其他提取。特别是,在执行下一个获取之前,无需value进行单个获取。但是,逐行获取关系数据库在技术上效率低下,实际上是一种反模式。因此,我想实现一个缓冲区,该缓冲区可获取一批比如说1万行的数据,将它们提供服务直到该批数据为空,然后再获取下一个数据。但是,由于随后引入了异步事件的依赖性,要实现这一点,将需要一个异步迭代器(Symbol.asyncIterator)。

要使GraphQL's reference implementation(graphql-js和/或express-graphql)接受异步Iterator,我该怎么办? 请注意,我要避免使用Apollo GraphQL

还是对象流可能是一种解决方案?

我们将不胜感激。

2 个答案:

答案 0 :(得分:0)

GraphQL.js在后台使用iterall。为了支持异步可迭代,底层代码必须使用该库中的forAwaitEach方法,而不是现在使用的forEach方法。这可能是可行的,但我不确定它是否不会破坏其他功能。

如果您只想获取一些任意大小的块中的所有people,则无需做任何特别花哨的事情:

async function getAllPeople () {
  const chunkSize = 10000
  const startId = 1
  const endId = await sequelize.models.person.max('id')
  const people = []

  let lower = startId
  let upper = startId + chunkSize

  while (upper < (endId + 1)) {
    const chunk = await sequelize.models.person.findAll({
      where: {
        id: {
          [Op.and]: {
            [Op.gte]: lower,
            [Op.lt]: upper,
          }
        }
      },
    })
    people.push(chunk)
    lower = lower + chunkSize
    upper = upper + chunkSize
  }

  return people
}

编辑:要解决内存问题,您必须有效地将有效负载分解为多个响应,并有一种方法可以将它们重新放在客户端。在Apollo的路线图上有一个@stream指令可以做到这一点,我认为有些人已经对其进行了试验,但是我想可能要过一段时间才能看到它的成熟实现。 @defer具有类似的机制,目前由Apollo支持,但可以在解析程序级别使用,因此在这种情况下它实际上没有帮助。

您可以使用subscriptions对其进行黑客入侵,而偶然使用异步迭代器。您仍然可能需要使用查询或变异来触发发送数据,但随后可以通过订阅将其发送给客户端。

不幸的是,考虑到当前的工具,我认为最简单的解决方案是仅对查询实施分页并让客户将总结果拼凑在一起。

答案 1 :(得分:0)

半个解决方案:使用流并将其转换为同步迭代器

由于希望GraphQL解析器返回同步迭代器,因此可以使用流将其数据馈送到此类迭代器中。考虑问题中发布的原始示例的以下解决方案。请注意,流行的ORM Sequelize不支持流,因此此处使用了另一个节点包knex

// Setup:
const knex = require('knex')
var dbCon = knex({
  client: 'pg',
  connection: {} // Define host, user, password, db (see knex docu)
})

// Get records as stream
var peopleStream = dbCon.select('*').from('people').stream()

// Serve stream within an synchronous iterator
var iter = {
  [Symbol.iterator]: () => {
    return this
  },
  next: function() {
    let v = peopleStream.read() || null
    console.log(JSON.stringify(v)) // Check, if it works.
    return {
      done: v === null,
      value: v
    }
  }
} 

但是,这实际上只是解决方案的一半,因为只能按所示方式利用生成流的数据源-依次将其轻松转换为同步迭代器,如下所示。以我的拙见,GraphQL的参考实现迫切需要支持异步迭代器作为解析器的结果值。有关更多详细信息,请参见this feature request