使用另一个字段的值更新MongoDB字段

537
在 MongoDB 中,是否可以使用另一个字段的值更新一个字段的值?相当于 SQL 中的某些语句:
UPDATE Person SET Name = FirstName + ' ' + LastName

而 MongoDB 的伪代码则是:

db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );

花括号不匹配...如果我让它工作,我会纠正它。 - wojand
12个回答

548

最好的方法是在4.2+版本中使用聚合管道(update文档)和updateOneupdateManyupdate(大多数语言驱动程序已弃用)集合方法。

MongoDB 4.2+

4.2版本还引入了$set管道阶段操作符,它是$addFields的别名。我将在这里使用$set,因为它与我们要实现的内容相符。

db.collection.<update method>(
    {},
    [
        {"$set": {"name": { "$concat": ["$firstName", " ", "$lastName"]}}}
    ]
)

请注意,方法的第二个参数中的方括号指定了一个聚合管道,而不是普通的更新文档,因为使用简单的文档将无法正常工作。

MongoDB 3.4+

在3.4+版本中,您可以使用$addFields$out聚合管道运算符。

db.collection.aggregate(
    [
        { "$addFields": { 
            "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
        }},
        { "$out": <output collection name> }
    ]
)

请注意,这不会更新您的集合,而是替换现有的集合或创建一个新的集合。此外,对于需要“类型转换”的更新操作,您需要进行客户端处理,并根据操作的不同,可能需要使用find()方法而不是.aggreate()方法。
MongoDB 3.2和3.0
我们通过$project文档并使用$concat字符串聚合运算符来返回连接的字符串来实现此目的。然后,您可以迭代游标并使用$set更新运算符使用批量操作将新字段添加到您的文档中,以实现最大效率。
聚合查询:
var cursor = db.collection.aggregate([ 
    { "$project":  { 
        "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
    }}
])

MongoDB 3.2或更高版本

您需要使用bulkWrite方法。

var requests = [];
cursor.forEach(document => { 
    requests.push( { 
        'updateOne': {
            'filter': { '_id': document._id },
            'update': { '$set': { 'name': document.name } }
        }
    });
    if (requests.length === 500) {
        //Execute per 500 operations and re-init
        db.collection.bulkWrite(requests);
        requests = [];
    }
});

if(requests.length > 0) {
     db.collection.bulkWrite(requests);
}

MongoDB 2.6和3.0

从这个版本开始,您需要使用现在已被弃用的Bulk API及其相关方法

var bulk = db.collection.initializeUnorderedBulkOp();
var count = 0;

cursor.snapshot().forEach(function(document) { 
    bulk.find({ '_id': document._id }).updateOne( {
        '$set': { 'name': document.name }
    });
    count++;
    if(count%500 === 0) {
        // Excecute per 500 operations and re-init
        bulk.execute();
        bulk = db.collection.initializeUnorderedBulkOp();
    }
})

// clean up queues
if(count > 0) {
    bulk.execute();
}

MongoDB 2.4

cursor["result"].forEach(function(document) {
    db.collection.update(
        { "_id": document._id }, 
        { "$set": { "name": document.name } }
    );
})

17
4.2+ 不起作用。MongoError:在 'name.$concat' 中,以美元符号($)为前缀的字段 '$concat' 对于存储无效。 - Josh Woodcock
2
@JoshWoodcock,我认为你在运行查询时打错了一个字母。建议你仔细检查一下。 - styvane
26
如果你遇到了与@JoshWoodcock描述相同的问题:请注意4.2+的答案描述了一个“聚合管道”,所以不要错过第二个参数中的“方括号”!请注意不要改变原文的意思。 - philsch
2
这个解决方案可以做到相同的事情,但是是否可能将两个数字相加而不是将两个字符串连接在一起? - Isaac Vidrine
5
他们要改几次才会变成一个笑话? - ajsp
显示剩余14条评论

263
你应该进行迭代。对于你的特定情况:
db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);

6
如果在你的find()和save()之间,另一个用户更改了文档,会发生什么? - UpTheCreek
3
没错,但在字段之间复制不应该要求事务具有原子性。 - UpTheCreek
3
请注意,save()方法会完全替换文档,建议使用update()方法。 - Carlos Melo
12
这段代码的意思是:更新db数据库中person集合中_id等于elem._id的文档,将该文档的name字段更新为elem.firstname + ' ' + elem.lastname的值。 - Philipp Jardas
1
我创建了一个名为 create_guid 的函数,只有在使用 forEach 迭代时才会为每个文档生成一个唯一的 guid(即仅仅在带有 mutli=trueupdate 语句中使用 create_guid 会导致所有文档生成相同的 guid)。这个答案对我非常有效。+1 - rmirabelle
显示剩余7条评论

108

自 MongoDB 3.4 起,似乎有一种高效的方法可以实现此操作,请参见styvane 的答案


以下是已过时的回答

您无法在更新中引用文档本身(暂时还不支持)。您需要通过函数迭代处理每个文档并进行更新。请参见此答案以获取示例,或者使用服务器端eval(),请参见此答案


31
今天仍然适用吗? - Christian Engel
3
@ChristianEngel: 看起来是这样的。我在MongoDB文档中没有找到任何关于在“update”操作中引用当前文档的内容。这个相关的功能请求也仍然未解决。 - Niels van der Rest
4
2017年4月仍然有效吗?或者已经有新功能可以实现这个吗? - Kim
1
@Kim 看起来它仍然有效。此外,@niels-van-der-rest在2013年指出的功能请求仍处于“OPEN”状态。 - Danziger
8
这个回答已经不再有效,请看一下@styvane的答案。 - aitchkhan

48

如果您的数据库活动频繁,您可能会遇到更新正在活跃更改记录的问题,出于这个原因,我建议使用snapshot()

db.person.find().snapshot().forEach( function (hombre) {
    hombre.name = hombre.firstName + ' ' + hombre.lastName; 
    db.person.save(hombre); 
});

http://docs.mongodb.org/manual/reference/method/cursor.snapshot/


2
如果在find()和save()之间另一个用户编辑了person,会发生什么?我有一个情况,在同一对象上可以进行多个调用,根据其当前值更改它们。第二个用户应该等待读取,直到第一个用户完成保存。这样做是否可以实现? - Marco
8
е…ідәҺ snapshot()пјҡиҮӘMongo Shell v3.2ејҖе§ӢпјҢ$snapshotиҝҗз®—з¬ҰеңЁMongo Shellдёӯе·Іиў«ејғз”ЁгҖӮ еңЁMongo ShellдёӯпјҢиҜ·ж”№з”Ёcursor.snapshot()гҖӮй“ҫжҺҘ - ppython

40

自Mongo 4.2版本起,db.collection.update()方法支持使用聚合管道,从而能够根据另一个字段更新/创建字段。

// { firstName: "Hello", lastName: "World" }
db.collection.updateMany(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }]
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • 第一部分 {} 是匹配查询,用于过滤要更新的文档(在我们的情况下是所有文档)。

  • 第二部分 [{ $set: { name: { ... } }] 是更新聚合管道(注意方括号表示使用聚合管道)。$set 是一个新的聚合操作符,是$addFields的别名。


2
对我起作用了。将一个字段分配给另一个字段而不进行连接,它起作用了。谢谢! - Mosheer
你的第一点和第三点有什么区别?{}代表所有文档,那么{multi: true}又是什么意思呢? - Coder17
@Coder17 第一部分{}是筛选条件部分: 例如您可能想要更新doc1doc2,但不想更新doc3。如果没有第三部分,默认情况下该更新将仅应用于一个文档,例如doc1,而doc2则不会被更新。请注意,您还可以使用db.collection.updateMany来摆脱第三个参数。 - Xavier Guihot
此时,我们将使用updateMany。 - David

16

关于这个答案,根据这个更新,快照功能在3.6版本中已经被弃用。因此,在3.6及以上版本中,可以通过以下方式执行操作:

db.person.find().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);

9

update() 方法接受聚合管道作为参数,例如:

db.collection_name.update(
  {
    // Query
  },
  [
    // Aggregation pipeline
    { "$set": { "id": "$_id" } }
  ],
  {
    // Options
    "multi": true // false when a single doc has to be updated
  }
)

使用聚合管道可以设置或取消现有值的字段。

注意:使用字段名称时,要在其前面加上$,以指定需要读取的字段。


1
仅适用于MongoDB 4.2及更高版本。请参见:https://dev59.com/CW865IYBdhLWcg3wHK7u#37280419 - steampowered
感谢您指出这一点。该解决方案适用于MongoDb 4.2+版本。 - Yuvaraj Anbarasan

8

我尝试了上面的解决方案,但发现对于大量数据不太适用。后来,我发现了流功能:

MongoClient.connect("...", function(err, db){
    var c = db.collection('yourCollection');
    var s = c.find({/* your query */}).stream();
    s.on('data', function(doc){
        c.update({_id: doc._id}, {$set: {name : doc.firstName + ' ' + doc.lastName}}, function(err, result) { /* result == true? */} }
    });
    s.on('end', function(){
        // stream can end before all your updates do if you have a lot
    })
})

1
这有什么不同吗?蒸汽会被更新活动限制吗?你有任何相关的参考资料吗?Mongo文档相当贫乏。 - Nico

2

以下是我们为约150,000条记录复制一个字段到另一个字段所想出的解决方案。虽然需要大约6分钟,但仍然比实例化并迭代同样数量的Ruby对象要少得多。

js_query = %({
  $or : [
    {
      'settings.mobile_notifications' : { $exists : false },
      'settings.mobile_admin_notifications' : { $exists : false }
    }
  ]
})

js_for_each = %(function(user) {
  if (!user.settings.hasOwnProperty('mobile_notifications')) {
    user.settings.mobile_notifications = user.settings.email_notifications;
  }
  if (!user.settings.hasOwnProperty('mobile_admin_notifications')) {
    user.settings.mobile_admin_notifications = user.settings.email_admin_notifications;
  }
  db.users.save(user);
})

js = "db.users.find(#{js_query}).forEach(#{js_for_each});"
Mongoid::Sessions.default.command('$eval' => js)

0

MongoDB 4.2+ Golang

result, err := collection.UpdateMany(ctx, bson.M{},
    mongo.Pipeline{
        bson.D{{"$set",
          bson.M{"name": bson.M{"$concat": []string{"$lastName", " ", "$firstName"}}}
    }},
)
        

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