【问题标题】:GraphQL dataloader with Firebase Firestore带有 Firebase Firestore 的 GraphQL 数据加载器
【发布时间】:2021-05-31 20:55:26
【问题描述】:

我正在为这个项目使用 Firebase Firestore,我正在尝试实现数据加载器以减少对服务器的请求。

在这种特殊情况下,我有一个结构如下的 Category 集合: (我将使用 typescript 来更好地定义我的代码)

type Category = {
    name: String
    parent?: Category
}

当我查询 graphql 端点时,我会在解析器中验证该类别是否有父类,如果存在,我会查询数据库以获取详细信息。数据加载器来了。

这是我的 Firestore 的 Dataloader 实现:

 private dataLoader = new DataLoader(async (ids: readonly string[]) => {
    // firestore limits batches to 10
    let batches: Category[] = [];
    const tmp = [...ids];
    while (tmp.length) {
        const batch = tmp.splice(0, 10);
        const result = await this.collection.where(FieldPath.documentId(), 'in', [...batch]).get();
        batches = [...batches, ...result.docs.map(d => ({ id: d.id, ...d.data() } as Category))];
    }

    return batches;
});

解析器本身调用此方法

public async getById(id: string): Promise<Category> {
    const foo = await this.dataLoader.load(id);
    console.log('required', id, 'received', foo.id);
    return foo;
}

但正如您在此示例输出中看到的那样,它搞砸了很多时间:

required ztdOcVv9JdNp6U54ySed received 4jXbScd1qTwPStXocaoc
required Qnb86GsOy0gSgGxmMqYR received NeuCCRrJyurQTxvlAVhs
required NeuCCRrJyurQTxvlAVhs received Qnb86GsOy0gSgGxmMqYR
required 4jXbScd1qTwPStXocaoc received ztdOcVv9JdNp6U54ySed

我的理解是某个地方的异步会导致一些麻烦,但不知道在哪里或如何解决这个问题。这就是我在这里寻求帮助的原因。

提前致谢

【问题讨论】:

  • 这些看起来像是从 Firestore 自动生成的 ID 字符串。当您将新文档添加到集合中时,Firestore 会自动生成文档标识符。签入 Firebase Firestore 控制台进行检查。
  • @Andrew 感谢您的评论并对延误表示抱歉。它们实际上是自动生成的 ID。它们存在于数据库中。只是不是我需要的。它们不匹配,我不明白这个输出是如何生成的。完全没有意义
  • 您可以在您的问题中添加数据库结构的副本吗?你希望看到什么?你提到它搞砸了,但我不确定你的描述是怎么回事。
  • Andrew,正如您可以从类型代码中推断的那样,只是一个字段、名称和一个称为 parent 的可选字段,当存在时,它是对同一集合中另一个类别的文档引用。在数据加载器中,我传递了一批 id(十个一组,因为 Firebase 本身的限制),它应该返回相关项目。 Instedad,它返回“洗牌结果”,正如您在示例输出中看到的那样。
  • 好吧,通读这篇文章,我想指出您没有订购结果,您正在查看批次中包含的任何结果......所以真的是订单你得到的不是“洗牌”,而是它在 Firestore 中保存的顺序。其中,查看第一个字符是按字母顺序排列的,这意味着它按字符值排序 (4

标签: typescript firebase google-cloud-firestore graphql graphql-js


【解决方案1】:

您需要在 DataLoader 函数中返回 ids 的循环,将获取的对象(在本例中为类别)映射到相应的 id。

这是我如何实现类似的东西:

async function getItemsByIds(ids: string[]) {
  // where in supports max 10 values
  // https://firebase.google.com/docs/firestore/query-data/queries
  const chunks = _.chunk(ids, 10);
  const snapshots = await Promise.all(
    chunks.map((chunk) =>
      admin
        .firestore()
        .collection('items')
        .where(admin.firestore.FieldPath.documentId(), 'in', chunk)
        .get()
    )
  );
  return snapshots.flatMap((snapshot) => {
    return snapshot.docs.map((doc) => ({
      id: doc.id,
      ...doc.data(),
    }));
  });
}

const itemDataLoader = new DataLoader(async (ids) => {
  const items = await getItemsByIds(ids);
  return ids.map((id) => {
    const item = items.find((item) => item.id === id);
    if (!item) {
      return new Error('Item not found');
    }
    return item;
  });
})

注意:我使用 lodash 帮助每个请求分块 10 个项目并取消分块数组。

【讨论】:

  • 我只返回null 而不是使用new Error("item not found"),因为堆栈跟踪没有用,只会增加响应的膨胀。我还考虑构建一个 ids -> data 的映射,而不是在循环中重复调用数组上的 find 。虽然这里的代码看起来很简单,但性能会受到影响。
  • 使用 find 是罪魁祸首。现在结果是一致的!但现在我对@samthecodingman 如何实现数据加载器很感兴趣。 find 确实会对性能产生这种影响?
  • @samthecodingman 没有错,但就像它所依赖的一切一样。如果您将数千个 id 传递给数据加载器,您可能不应该为每条记录使用 find。另一件事;我刚刚查看了DataLoader docs,并注意到他们有一个maxBatchSize 选项。这可能会消除对 lodash 分块/平面映射的需要。
  • 跟进@jonespen 的观点,如果您使用maxBatchSize,您将不会遇到此问题。但是,如果您想探索性能上的差异,我已经使用生成的数据为它整理了一个JSBench。通过更改设置部分的第一行,您可以测试任意数量的 ID。
猜你喜欢
  • 2018-08-11
  • 2019-04-25
  • 2020-09-17
  • 1970-01-01
  • 1970-01-01
  • 2019-03-11
  • 2021-09-22
  • 1970-01-01
  • 2020-05-19
相关资源
最近更新 更多