当父文档可能不存在时更新 MongoDB 子文档

Update a MongoDB subdocument when the parent document might not exist

这是我的数据,由 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 个单独的查询来执行此操作,但我知道这不是最佳选择。我可以使用的最少查询数量是多少(当然,这些查询是什么)?

你基本上有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。因此,您可以应用它们一次或多次,并始终获得相同的结果。这在故障转移的情况下尤为重要。此外,如果发生故障,您的数据库不可能不一致或丢失 existing 数据。在最坏的情况下,评论 不会 更新。最后,即使在并发更新的情况下, 应该 也能保证数据的一致性(即:在这种情况下,一个更新将覆盖另一个,但你不应该最终拥有同一本书的两个文档或同一用户对同一本书的两条评论)。
后一点必须确认,因为这里已经晚了,所以我的分析可能有些可疑。

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