【问题标题】:$inc vs. $setOnInsert in MongoCollection.findOneAndUpdate()MongoCollection.findOneAndUpdate() 中的 $inc 与 $setOnInsert
【发布时间】:2016-11-23 16:27:46
【问题描述】:

我正在尝试在 mongodb 中实现 'server-side counter-versioned' 项目并尝试执行以下操作 /* 使用 Java API */

Document dbDoc = dbCollection.findOneAndUpdate(
        new Document("_id", "meta"), 
        new Document("$inc", new Document("version", 1))
             .append("$setOnInsert", new Document("version", 0)),
        new FindOneAndUpdateOptions().upsert(true)
                                     .returnDocument(ReturnDocument.AFTER));

假设的逻辑很简单:如果数据库中没有记录 - 从零开始计数(并且使用全新的对象),否则 - 递增计数器。

代码示例失败:'Cannot update 'version' and 'version' at the same time'

我的假设是,在 'upsert' 模式下,mongo 应该只在没有找到匹配项时使用“$setOnInsert”——但它以其他方式工作。

是否可以在一个原子 mongoDB 调用中实现这样的操作?


PS:MongoDB documentation 关于 findOneAndUpdate() 和 upsert() 是模糊的 - 至少我无法从他们的描述中理解为什么会出现这个错误。

这里也有类似的问题 - findAndModify fails with error: "Cannot update 'field1' and 'field1' at the same time - 已接受,但同样没有明确的理由。

【问题讨论】:

  • 我想添加这个例子,当你有一个嵌套对象的值:{_id: 1, hours: { 0:0, 1:0, 2:0 }} 并且你想要如果文档不存在,则添加和增加一个小时,如果存在,则只增加一个小时。其实这是不可能的,也不违反任何逻辑。

标签: mongodb


【解决方案1】:

您可以删除 $setOnInsert 的更新运算符,因为如果文档不存在,它将设置为 $inc 运算符中指定的值

https://docs.mongodb.com/v3.2/reference/operator/update/inc/#behavior

如果该字段不存在,则 $inc 创建该字段并将该字段设置为指定值。

来自 mongo shell 的示例:

> db.dropDatabase()
{ "ok" : 1 }
> db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {upsert: true, returnNewDocument: true})
{ "_id" : "meta", "version" : 1 }
> db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {upsert: true, returnNewDocument: true})
{ "_id" : "meta", "version" : 2 }
> db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {upsert: true, returnNewDocument: true})
{ "_id" : "meta", "version" : 3 }
> db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {upsert: true, returnNewDocument: true})
{ "_id" : "meta", "version" : 4 }
> db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {upsert: true, returnNewDocument: true})
{ "_id" : "meta", "version" : 5 }

如果您需要在第一次初始插入时设置给定版本,则 mongodb 不支持任何运算符以原子方式支持此操作,但以下是安全的并且是常见的解决方法:

> db.dropDatabase()
{ "dropped" : "test", "ok" : 1 }
> function updateMeta(){
...   function update(){
...     return db.test.findOneAndUpdate({_id: "meta"}, { $inc: { version: 1} }, {returnNewDocument: true});
...   }
...
...   var result = update();
...
...   if(result === null){
...     db.test.insert({_id: "meta", version: -10});
...     result = update();
...   }
...
...   return result;
... }
>
> updateMeta()
{ "_id" : "meta", "version" : -9 }
> updateMeta()
{ "_id" : "meta", "version" : -8 }
> updateMeta()
{ "_id" : "meta", "version" : -7 }
> updateMeta()
{ "_id" : "meta", "version" : -6 }
> updateMeta()
{ "_id" : "meta", "version" : -5 }
>

【讨论】:

  • 这不是我所需要的——我想以“0”(零)(或者可能是其他数字)作为初始计数器值。
  • 这种变化不是一个原子的mongoDB调用,它意味着从许多不同的客户端并行执行相同的函数会导致不可预知的结果。
  • @XtraCoder 我们利用了上面的原子操作。我们首先尝试更新文档,如果失败,则尝试确保添加文档,然后更新新文档。插入可能会发生多次,但只有一次会成功并返回{ "nInserted" : 1 },然后所有后续更新将自动发生。这更像是一种解决方法,可以回答您无法在一个原子操作中执行此操作的问题。
  • 在你的具体代码示例中,10个不同的客户端可以同时输入update(),他们都会收到null作为结果,然后他们都会执行10次insert操作,有些会失败因为重复,从而导致结果不是很可预测。这不是所谓的原子
  • 插入不会失败,它只会返回inserted=0的结果,整个函数是可预测的,因为即使并发客户端按插入方法,它仍然不会不同步只插入一个文档。除非你的机器或网络真的很慢,否则在插入之前并发命中更新的概率会很低。但正如我所说,上面的方法在并发时是安全的。
猜你喜欢
  • 2017-06-05
  • 1970-01-01
  • 2017-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-11
  • 1970-01-01
  • 2011-08-24
相关资源
最近更新 更多