如何在MongoDB中部分更新对象,使新对象覆盖/合并现有对象

278

假设在 MongoDB 中有这个文档:

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
   }
}

需要保存一个带有来自外部世界的新信息的对象,其中包括 param2param3

var new_info = {
    param2 : "val2_new",
    param3 : "val3_new"
};
我希望合并/覆盖新字段到对象的现有状态,这样param1就不会被删除。
执行以下操作:
db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

这会导致MongoDB按照要求执行,并将some_key设置为该值,替换旧值。

{
   _id : ...,
   some_key: { 
      param2 : "val2_new",
      param3 : "val3_new"
   }
}

如何让MongoDB仅更新新字段(而不需要明确地逐个声明它们)?以获取以下结果:

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2_new",
        param3 : "val3_new"
   }
}

我正在使用Java客户端,但是任何示例都将不胜感激


3
请投票支持$merge问题:https://jira.mongodb.org/browse/SERVER-21094。 - Ali
@Ali 这条评论是给看到这篇文章的新手们的:不幸的是,他们决定不实现 $merge 操作符。 - Tal Kohavy
19个回答

152

我用自己的函数解决了这个问题。如果你想要更新文档中的特定字段,需要明确指出它。

例如:

{
    _id : ...,
    some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
    }
}

如果你只想更新 param2 ,那么这样做是错误的:

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } }  //WRONG

你必须使用:

db.collection.update(  { _id:...} , { $set: { "some_key.param2" : new_info  } } 

所以我写了一个类似这样的函数:

function _update($id, $data, $options=array()){

    $temp = array();
    foreach($data as $key => $value)
    {
        $temp["some_key.".$key] = $value;
    } 

    $collection->update(
        array('_id' => $id),
        array('$set' => $temp)
    );

}

_update('1', array('param2' => 'some data'));

谢谢你的回复,它帮助了我。我能够按照以下方式修改用户集合中特定键的值:db.users.update( { _id: ObjectId("594dbc3186bb5c84442949b1") }, { $set: { resume: { url: "http://i.imgur.com/UFX0yXs.jpg" } } } ) - Luke Schoen
1
这个操作是原子的和隔离的吗?也就是说,如果有两个并行的线程试图设置同一文档的两个不同字段,是否会导致竞争条件和不可预测的结果? - so-random-dude
现在可以使用4.2聚合支持在更新查询中实现此功能-请查看我的答案-https://dev59.com/3Wkv5IYBdhLWcg3w4kl2#65007307 - s7vr

127
如果我理解问题正确的话,你想要使用另一个文档的内容更新一个文档,但仅更新不存在的字段,而完全忽略已经设置的字段(即使是另一个值)。没有单个命令可以完成这个任务。你需要先查询文档,确定你想要 $set 的内容,然后进行更新(使用旧值作为匹配过滤器,以确保在此期间不会出现并发更新)。另一种理解你的问题的方式是,你满意使用 $set,但不想显式地设置所有字段。那么你如何传递数据呢?你知道可以这样做:
db.collection.update(  { _id:...} , { $set: someObjectWithNewData } 

18
没有$multi参数,有一个multi选项(就是你链接的那个选项),但它不会影响字段如何更新。它控制着你是更新单个符合查询参数的文档还是所有符合查询参数的文档。 - Bob Kerns
2
这根本没有任何意义。在处理Mongo时,最好也为更新操作使用原子操作。首先读取它会导致脏读,每当其他人更改您的数据时。而且由于Mongo没有事务,它将愉快地为您损坏数据。 - froginvasion
1
@froginvasion:您可以使用旧值作为更新的匹配过滤器,使其具有原子性。这样,如果同时存在冲突的并发更新,您的更新将失败。然后,您可以检测到这一点,并相应地采取行动。这被称为乐观锁定。但是,是的,这取决于您的应用程序是否正确实现它,Mongo本身没有内置事务。 - Thilo
1
提供文档链接总是很有用的,我认为 https://docs.mongodb.com/manual/reference/method/db.collection.update/ - Eduardo Pignatelli
1
@Thilo 如果您以这种方式设置匹配条件,就无法知道服务器是否没有此资源(404状态代码)或者是(412状态代码)。 - eirikrl
显示剩余4条评论

32
你可以使用点符号来访问和设置对象内部深层的字段,而不影响这些对象的其他属性。
给定上面指定的对象:
> db.test.insert({"id": "test_object", "some_key": {"param1": "val1", "param2": "val2", "param3": "val3"}})
WriteResult({ "nInserted" : 1 })

我们可以仅更新some_key.param2some_key.param3

> db.test.findAndModify({
... query: {"id": "test_object"},
... update: {"$set": {"some_key.param2": "val2_new", "some_key.param3": "val3_new"}},
... new: true
... })
{
    "_id" : ObjectId("56476e04e5f19d86ece5b81d"),
    "id" : "test_object",
    "some_key" : {
        "param1" : "val1",
        "param2" : "val2_new",
        "param3" : "val3_new"
    }
}
你可以深入挖掘。这也有助于向对象添加新属性而不影响现有属性。

我能添加一个新的键值对吗?例如:"some key" : {"param1" : "val1", "param2" : "val2_new", "param3" : "val3_new", "param4" : "val4_new", } - Urvashi Soni

27

从Mongo 4.2开始,db.collection.updateMany()(或db.collection.update())可以接受聚合管道,这允许使用聚合操作符,例如$addFields,它会输出输入文档中所有现有字段和新添加的字段:

var new_info = { param2: "val2_new", param3: "val3_new" }

// { some_key: { param1: "val1", param2: "val2", param3: "val3" } }
// { some_key: { param1: "val1", param2: "val2"                 } }
db.collection.updateMany({}, [{ $addFields: { some_key: new_info } }])
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
  • 第一部分{}是匹配查询,筛选要更新的文档(在这种情况下为所有文档)。

  • 第二部分[{ $addFields: { some_key: new_info } }]是更新聚合管道:

    • 请注意使用聚合管道的方括号符号。
    • 由于这是聚合管道,我们可以使用$addFields
    • $addFields执行您需要的操作:更新对象,以便新对象将叠加/合并到现有对象中:
    • 在本例中,{ param2: "val2_new", param3: "val3_new" }将与现有的some_key合并,通过保持param1不变并添加或替换param2param3

1
哇,这就是我上个小时一直在寻找的东西。谢谢。这应该是被接受的答案,并需要更多的赞。 - garg10may

27
最佳解决方案是从对象中提取属性并使它们成为扁平的点符号键值对。你可以使用例如这个库:https://www.npmjs.com/package/mongo-dot-notation。它有一个 .flatten 函数,允许你将对象转换为一组扁平的属性,然后可以将其传递给 $set 修改器,而不必担心现有数据库对象的任何属性会被删除或覆盖。引用自 mongo-dot-notation 文档:
var person = {
  firstName: 'John',
  lastName: 'Doe',
  address: {
    city: 'NY',
    street: 'Eighth Avenu',
    number: 123
  }
};



var instructions = dot.flatten(person)
console.log(instructions);
/* 
{
  $set: {
    'firstName': 'John',
    'lastName': 'Doe',
    'address.city': 'NY',
    'address.street': 'Eighth Avenu',
    'address.number': 123
  }
}
*/

然后它会形成完美的选择器 - 它将仅更新给定的属性。 编辑:有时我喜欢当考古学家;)


12

10
我用以下方法成功地完成了它:
db.collection.update(  { _id:...} , { $set: { 'key.another_key' : new_info  } } );

我有一个函数可以动态处理我的个人资料更新

function update(prop, newVal) {
  const str = `profile.${prop}`;
  db.collection.update( { _id:...}, { $set: { [str]: newVal } } );
}

注意:'profile'是特定于我的实现的,它只是您想要修改的键的字符串。

6

在基于聚合的更新中,您可以使用$mergeObjects。类似以下内容:

db.collection.update(
   { _id:...},
   [{"$set":{
      "some_key":{
        "$mergeObjects":[
          "$some_key",
          new info or { param2 : "val2_new", param3 : "val3_new"}
       ]
      }
   }}]
)

更多示例在这里


将多个文档合并成一个文档。 - Oliver Dixon

5

您可以使用upsert操作,MongoDB中的此操作用于将文档保存到集合中。如果文档与查询条件匹配,则执行更新操作,否则将插入新文档到集合中。

类似下面的示例:

db.employees.update(
    {type:"FT"},
    {$set:{salary:200000}},
    {upsert : true}
 )

3
    // where clause DBObject
    DBObject query = new BasicDBObject("_id", new ObjectId(id));

    // modifications to be applied
    DBObject update = new BasicDBObject();

    // set new values
    update.put("$set", new BasicDBObject("param2","value2"));

   // update the document
    collection.update(query, update, true, false); //3rd param->upsertFlag, 4th param->updateMultiFlag

如果您需要更新多个字段:
        Document doc = new Document();
        doc.put("param2","value2");
        doc.put("param3","value3");
        update.put("$set", doc);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接