【问题标题】:Update a MongoDB subdocument when the parent document might not exist当父文档可能不存在时更新 MongoDB 子文档
【发布时间】:2015-07-29 07:20:19
【问题描述】:

这是我的数据,由 books 集合和 books.reviews 子集合组成。

books = [{
    _id: ObjectId("5558f40ad498c653748cf045"),
    title: "Widget XYZ",
    isbn: 1234567890,
    reviews: [
        {
            _id: ObjectId("5558f40ad498c653748cf046"),
            userId: "01234",
            rating: 5,
            review: "Yay, this great!"
        },
        {
            _id: ObjectId("5558f40ad498c653748cf047"),
            userId: "56789",
            rating: 3,
            review: "Meh, this okay."
        }
    ]
}]

在 Node.js(使用 Mongoose)中,我需要用户能够通过表单提交评论,将评论和图书的 isbn 发送到服务器,条件如下:

  1. 如果该书不存在特定的 isbn,请添加它,然后添加评论(它显然不存在)。
  2. 如果这本书确实存在...
    • 如果此书此用户的评论不存在,请添加。
    • 如果这本书的评论确实存在此用户,请更新它。

我可以使用 4 个单独的查询来做到这一点,但我知道这不是最优的。我可以使用的最少查询数量是多少(当然,这些查询是什么)?

【问题讨论】:

  • 您如何检查评论是否已存在?我可以在一个查询中想出一种方法来做到这一点,但它假设一个新的评论。

标签: node.js mongodb subdocument


【解决方案1】:

你基本上有3种情况:

  1. 书和评论都存在。这是一个简单的$set
  2. 该书存在,但评论不存在。这需要$push
  3. 这本书不存在。这需要{upsert:1}$setOnInsert

我无法找到一种方法来统一其中任何两个而不影响数据完整性以防万一发生故障(请记住,MongoDB 没有原子事务)。

所以我的最好的主意如下:

// Case 1:
db.books.update({isbn:'1234567890',
                 review: { $elemMatch: {userID: '01234'}}},
                {$set: {'review.$.rating': NEW_RATING}}
               )

// Case 2:
db.books.update({isbn:'1234567890',
                 review: { $not: { $elemMatch: {userID: '01234'}}}},
                {$push: {review: {rating: NEW_RATING, userID:'01234'}}}
               )

// Case 3:
db.books.update({isbn:'1234567890'},
                {$setOnInsert: {review: [{rating: NEW_RATING, userID:'01234'}]}},
                {upsert:1}
               )

您可能会盲目地以原始方式运行这三个更新,因为它们之间没有重叠的情况。事情的美妙之处在于所有这些操作都是idempotent。因此,您可以应用它们一次或多次,并始终获得相同的结果。这在故障转移的情况下尤其重要。此外,您的数据库无法在发生故障时出现不一致或丢失现有数据。在最坏的情况下,评论没有更新。最后,即使在并发更新的情况下,这应该保证数据一致性(即:在这种情况下,一个更新将覆盖另一个,但你不应该最终为同一本书或两个评论拥有两个文档同一本书的同一用户)。
后面的一点必须得到证实,因为这里已经很晚了,所以我的分析可能有些怀疑。

最后一点,如果您想减少 MongoDB 和您的应用程序之间的往返次数,您可以查看update database command 允许您在一个命令中包装多个更新。

【讨论】:

  • 我花了 20 分钟试图想办法打破这个解决方案,但它似乎真的很可靠。但是,如果我在“案例 2”中添加 {upsert:1} 会发生什么?
  • @Horace 如果将{upsert:1} 添加到案例2,它不再是幂等的:两次发出相同的命令将导致多次插入。这是可以接受的,当且仅当您在故障转移时格外小心。也许只是在步骤 1 没有匹配项时才运行步骤 2(顺便说一句,只有在步骤 2 也没有匹配项时才运行步骤 3);这是一个简单的if ... else if .... else ... 逻辑——但您现在在代码维护方面更容易受到“人为”错误的影响。 OTOH,这减少了数据库的负载。但是您不能再只发送包含所有三个更新的批量更新...
  • 谢谢,这是一个很好的答案。
  • 这很好用。我正在通过BookModel.db.db.command({update:'books', ...}) 一次调用服务器来执行您的案例,而且速度非常快。不过,我担心会绕过 Mongoose 的模式执行。
猜你喜欢
  • 2014-03-06
  • 1970-01-01
  • 1970-01-01
  • 2011-08-04
  • 1970-01-01
  • 2015-08-15
  • 1970-01-01
相关资源
最近更新 更多