MongoDB:upsert子文档

时间:2014-05-05 10:49:37

标签: mongodb mongodb-query upsert

我的文档类似于bars.name上的唯一索引:

{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }

。我想要更新{ name: 'foo', 'bars.name': 'qux' }$set: { 'bars.$.somefield': 2 }所在的子文档,或者在{ name: 'qux', somefield: 2 }下创建一个{ name: 'foo' }的新子文档。

是否可以使用带有upsert的单个查询来执行此操作,还是必须发出两个单独的查询?

相关:'upsert' in an embedded document(建议更改架构以将子文档标识符作为键,但这是两年前的事情,我现在想知道是否有更好的解决方案。)< / p>

5 个答案:

答案 0 :(得分:41)

目前还没有更好的解决方案,所以也许有一个解释。

假设您的文档具有您显示的结构:

{ 
  "name": "foo", 
  "bars": [{ 
       "name": "qux", 
       "somefield": 1 
  }] 
}

如果你做这样的更新

db.foo.update(
    { "name": "foo", "bars.name": "qux" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

然后一切都很好,因为找到了匹配文件。但是如果你改变&#34; bars.name&#34;的价值:

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

然后你会失败。这里唯一真正改变的是,在MongoDB 2.6及更高版本中,错误更加简洁:

WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 16836,
        "errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
    }
})

这在某些方面更好,但你真的不想&#34; upsert&#34;无论如何。你想要做的是将元素添加到数组中,其中&#34; name&#34;目前不存在。

所以你真正想要的是&#34;结果&#34;来自没有&#34; upsert&#34;的更新尝试标记以查看是否有任何文档受到影响:

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } }
)

作出回应:

WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })

因此,当修改后的文档为0时,您就知道要发布以下更新:

db.foo.update(
    { "name": "foo" },
    { "$push": { "bars": {
        "name": "xyz",
        "somefield": 2
    }}
)

没有其他方法可以完全按照自己的意愿行事。由于数组的添加不是严格的&#34; set&#34;操作类型,您无法使用$addToSet"bulk update"功能相结合,以便您可以&#34;级联&#34;你的更新请求。

在这种情况下,您似乎需要检查结果,否则接受读取整个文档并检查是否更新或在代码中插入新的数组元素。

答案 1 :(得分:3)

如果您不介意稍微更改架构并具有如此结构:

{ "name": "foo", "bars": { "qux": { "somefield": 1 },
                           "xyz": { "somefield": 2 },
                  }
}

您可以一次性执行操作。 重申'upsert' in an embedded document完整性

答案 2 :(得分:1)

如果更新依赖于对正在更新的记录的引用(例如,更新x =&gt; x + 1),则无法执行此操作。发出2个单独的命令(set然后插入)导致竞争条件无法通过检查重复项来解决,因为如果您的插入因重复而被拒绝,您将失去更新的效果(例如,x不会在上面的例子中正确递增)。如果MongoDB添加了这个功能,那就太好了,因为实现嵌入式文档的能力对MongoDB来说对于某些应用来说是一个很大的吸引力。

答案 3 :(得分:0)

有两种方法可以做到这一点-但它仍然可以在bulkWrite中使用。

这很重要,因为在我的情况下,这是最大的麻烦。使用此解决方案,您无需收集第一个查询的结果,从而可以根据需要执行批量操作。

以下是为您的示例运行的两个连续查询:

// Update subdocument if existing
collection.updateMany({
    name: 'foo', 'bars.name': 'qux' 
}, {
    $set: { 
        'bars.$.somefield': 2 
    }
})
// Insert subdocument otherwise
collection.updateMany({
    name: 'foo', $not: {'bars.name': 'qux' }
}, {
    $push: { 
        bars: {
            somefield: 2, name: 'qux'
        }
    }
})

如果多个应用程序同时向数据库写入数据,这还具有不破坏数据/竞争条件的附加优点。如果两个应用程序同时运行相同的查询,则不会冒在文档中包含两个bars: {somefield: 2, name: 'qux'}子文档的风险。

答案 4 :(得分:0)

我在挖掘相同的功能,发现在4.2或更高版本中,MongoDB提供了一个名为Update with aggregation pipeline的新功能。
如果将此功能与其他一些技术结合使用,则可以通过单个查询实现 upsert子文档操作。

这是一个非常冗长的查询,但是我相信,如果您知道在subCollection上没有太多记录,那么它是可行的。这是有关如何实现此目的的示例:

collection.update({ name: 'foo' }, [
    {
        $set: {
            bars: {
                $cond: {
                    if: { $not: ['$bars'] },
                    then: [{ name: 'xyz' }],
                    else: {
                        $cond: {
                            if: { $in: ['xyz', '$bars'] },
                            then: {
                                $map: {
                                    input: '$bars',
                                    as: 'bar',
                                    in: {}, // ...use another $cond to filter by name = xyz and update the inner element to the object you want.
                                },
                            },
                            else: { $concatArrays: ['$bars', [{ name: 'xyz' }]] },
                        },
                    },
                },
            },
        },
    },
])