【问题标题】:firebase firestore: Performing SQL-like joinsfirebase firestore:执行类似 SQL 的连接
【发布时间】:2021-03-26 14:30:27
【问题描述】:

我是 firebase 新手,但我有一个 users 集合,其中每个用户文档都有以下字段

  • username
  • avatar(头像)
  • name

每个用户都可以发表评论,评论将存储在带有这些字段的 comments 集合中

  • createdAt

  • username(评论的用户)

  • userAvatar(用户头像)

在检索 cmets 时,我可以将每条评论与评论的用户 (username) 和 userAvatar 一起显示。虽然每个评论都有静态的usernameuserAvatar 字段,但问题是如果我要在用户文档中更改用户的头像,我也想修改相关评论文档中的 userAvatar 以获得更多的数据一致性。 为了实现这一点,我考虑创建一个云函数触发器来收听用户的avatar 更新,以相应地更改评论userAvatar。但我很快意识到这在成本方面是一个糟糕的举措,因为每次用户文档内部发生更改时都会触发云功能,无论它是否是 avatar 更改。

所以我决定在评论文档中创建一个用户引用,因为它就像 SQL 数据库中的引用键一样工作

  • createdAt
  • user: db.doc('users/' + firebase.auth().currentUser.uid)

现在的主要问题是,如果我想从数据库中检索 15 个 cmets,对于每条评论,我们还必须获取用户信息。所以我们会有这样的东西

exports.getComments = async (req, res) => {
try {
  let comments = [];
  const data = await db
   .collection("comments")
   .orderBy("createdAt", "desc")
   .get();
  data.forEach((doc) => {
    let comment = doc.data();
    doc.data().user
     .get()
     .then(element => {
        comment.avatar = element.data().avatar;
        comment.username = element.data().username;
        comments.push(comment);
     })
    .catch(e => console.log(e));
  });
 return res.json(comments);
} catch (e) {
    console.log(e);
    return res.status(500).json({ e });
}
};

现在我想知道如果我有 1000 cmets 会怎样?在延迟方面,该应用不会提供良好的用户体验。不能同时从用户和评论集合中检索数据吗?

【问题讨论】:

  • 问题:如果我今天是罗伯特,我发表评论,明天改名为 Freddie - 谁发表评论?罗伯特还是弗雷迪?
  • @RobertKawecki 您可以将您的姓名更改为。因为如果您更改用户信息,评论只会使用用户参考,因此评论也会发生变化。但是我会让更改用户名变得困难。用户名将是电子邮件或文档 ID 的附加标识符

标签: node.js firebase google-cloud-firestore


【解决方案1】:

记住关于 Firestore 的两件事:

  • 查询执行时间取决于结果集的大小,而不是文档总数
  • 我们无法进行连接查询(这是执行时间限制的结果)*

想到一些解决方案来处理您的 cmets 和用户:

数据建模

? 在 cmets 中复制用户信息

这是您的第一种方法,它是在 NoSQL 中处理此类情况的建议方法,它被称为“反规范化”。正如您所指出的,这意味着当用户的名称或头像发生更改时,您必须使用 Cloud Function 修改所有关联的 cmets。
但是,正如 @RobertKawecki(或者是 Freddie??)所指出的,您可能应该保留初始名称和头像,因为它是正确的评论创建点
有关此技术的更多详细信息,请参阅Firebase presentation

? 将 cmets 存储在用户文档中

您可以解决问题并将 cmets 存储在用户文档的子集合中。然后您可以使用collection group 查询来检索所有 cmets。就我个人而言,我可能会走这条路;)

延迟优化

现在,如果您仍然不想在 cmets 中复制用户数据,可以通过几种方式提高初始代码的性能。

您的函数的当前延迟是 15 + 15 个时间单位,因为您从 cmets 集合中获取 15 个文档(我认为您忘记了代码中的 limit 子句),然后从 users 集合中获取 15 个文档(我想您忘记了在您的代码中添加Promise.all)。

?使用缓存

好的,这是基本的,不会带来什么改进。假设在您最后的 15 个 cmets 中,有一些属于同一个用户。如果您在检索用户时缓存用户,则可以避免多次查询同一用户。延迟将减少到 15 +(15 - 重复用户数)

⛷️ 并行化

在您的函数中,您首先获取 cmets,然后获取所需的用户文档。您可以使用stream API 并行化获取:流式传输您的 cmets,一旦您收到一个,并行获取关联的用户文档。该代码有点过于复杂,无法在此处键入。这将使延迟达到 15 + 1

☎️从前端获取数据

这个可以很好地节省延迟。您可以将 Firestore front-end SDKs 用于 Web 或移动设备,直接从前端查询您的数据库。在那里,您可以在 cmets 集合上使用 snapshot listener,并在每个收到的评论中获取关联的用户(就像在并行化解决方案中一样),并立即显示它。
这使延迟达到 2 ???。很难打败!您还可以获得离线支持!

* 附带说明,如果您维护一个连接表,则可以执行连接查询,但它对您的情况没有帮助,因为它需要另一个集合和云函数,并且不会减少延迟。我写了一篇关于它的文章here

【讨论】:

  • @LoouisCoulet 非常感谢您的回答,它非常清晰简洁。我最初使用 express API 设计我的应用程序以在后端执行查询,因为我认为它会使应用程序更轻,但我认为我会调整它以直接从前端查询。现在,对于这种情况(反规范化)或带有“子集合”的情况,我应该选择什么数据建模?
  • 我很高兴它有帮助!从前端查询是 IMO 的最佳选择。这样您就不再有延迟问题,因此您不需要以任何一种方式去规范化(每个评论文档中的用户信息,或每个用户文档中子集合中的 cmets)。您可以在不复制任何数据的情况下将用户和 cmets 分开。
猜你喜欢
  • 2020-12-17
  • 2015-12-12
  • 1970-01-01
  • 2019-02-03
  • 2020-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-16
相关资源
最近更新 更多