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


372

在MongoDB中,是否可以使用另一个字段中的值来更新字段的值?等效的SQL将类似于:

UPDATE Person SET Name = FirstName + ' ' + LastName

MongoDB伪代码将是:

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

Answers:


258

要做到这一点,最好的办法是在4.2以上版本,它允许更新文档和使用聚合管道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": "collection" }
    ]
)

请注意,这不会更新您的收藏集,而是替换现有的收藏集或创建一个新的收藏集。另外,对于需要“类型转换”的更新操作,您将需要客户端处理,并且根据操作,您可能需要使用find()方法来代替.aggreate()方法。

MongoDB 3.2和3.0

我们执行此操作的方法是通过查看$project文档并使用$concat字符串聚合运算符返回串联的字符串。我们从那里开始,然后循环游标并使用$setupdate运算符使用批量操作将新字段添加到文档中,以实现最大效率。

汇总查询:

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

从此版本开始,您需要使用现在不推荐使用的BulkAPI及其相关方法

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 } }
    );
})

我认为“ MongoDB 3.2或更高版本”的代码存在问题。由于forEach是异步的,因此通常不会在最后的bulkWrite中写入任何内容。
维克多·赫德福克

3
4.2+不起作用。MongoError:'name。$ concat'中以美元($)为前缀的字段'$ concat'无效。
乔什·伍德考克

@JoshWoodcock,我认为您正在运行的查询中有错字。我建议您仔细检查。
styvane

@JoshWoodcock它的工作原理很漂亮。请使用MongoDB Web Shell
styvane

2
对于遇到相同问题的人,@ JoshWoodcock描述:请注意,4.2+的答案描述了聚合管道,因此请不要错过第二个参数中的方括号
philsch

240

您应该遍历。对于您的具体情况:

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

4
如果另一个用户在find()和save()之间更改了文档,会发生什么情况?
UpTheCreek

3
是的,但是在字段之间进行复制不应要求事务是原子的。
UpTheCreek

3
重要的是要注意save()完全替代了文档。应该update()改用。
卡洛斯(Carlos)

12
怎么样db.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
菲利普·贾达斯

1
我创建了一个称为的函数create_guid,当以forEach这种方式进行迭代时,每个文档仅生成一个唯一的guid (即,简单地create_guidupdate语句中使用会mutli=true导致为所有文档生成相同的guid)。这个答案对我来说非常有效。+1
rmir​​abelle 2015年

103

从MongoDB 3.4开始,显然有一种方法可以有效地做到这一点,请参见styvane的答案


以下是过时的答案

您无法在更新中引用文档本身(尚未)。您需要遍历文档并使用函数更新每个文档。见这个答案的一个例子,或者这一个服务器端eval()


31
今天仍然有效吗?
Christian Engel

3
@ChristianEngel:看起来是这样。我在MongoDB文档中找不到任何提及update操作中对当前文档的引用的内容。此相关功能请求仍未解决。
Niels van der Rest 2013年

4
它在2017年4月仍然有效吗?还是已经有新功能可以做到这一点?

1
@Kim似乎仍然有效。另外,@ niels-van-der-rest在2013年指出的功能要求仍然存在OPEN
Danziger

8
这不再是有效的答案,请查看@styvane答案
aitchkhan

45

对于活动频繁的数据库,您的更新可能会影响到活动更改的记录,因此,我建议使用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()之间编辑人员,会发生什么情况?我有一种情况,可以对同一个对象进行多次调用,以根据其当前值更改它们。第二个用户应该等待阅读,直到第一个完成保存。这样可以做到吗?
Marco Marco

4
关于snapshot()Deprecated in the mongo Shell since v3.2. Starting in v3.2, the $snapshot operator is deprecated in the mongo shell. In the mongo shell, use cursor.snapshot() instead. 链接
ppython

10

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

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

9

开始于Mongo 4.2db.collection.update()可以接受聚合管道,最后允许基于另一个字段对字段进行更新/创建:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • 第一部分{}是匹配查询,过滤要更新的文档(在本例中为所有文档)。

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

  • 不要忘记{ multi: true },否则只会更新第一个匹配的文档。


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分钟的时间,但是与在相同数量的红宝石对象上进行实例化和迭代相比,它的资源消耗仍然大大减少。

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)

1

随着MongoDB的4.2以上版本,更新更灵活,因为它允许在其使用聚合管道updateupdateOneupdateMany。现在,您可以使用聚合运算符来转换文档,然后进行更新,而无需明确声明$set命令(而是使用$replaceRoot: {newRoot: "$$ROOT"}

在这里,我们使用聚合查询从MongoDB的ObjectID“ _id”字段中提取时间戳并更新文档(我不是SQL方面的专家,但我认为SQL不提供任何自动生成的带有时间戳的ObjectID,您必须自动创建该日期)

var collection = "person"

agg_query = [
    {
        "$addFields" : {
            "_last_updated" : {
                "$toDate" : "$_id"
            }
        }
    },
    {
        $replaceRoot: {
            newRoot: "$$ROOT"
        } 
    }
]

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})

你不需要{ $replaceRoot: { newRoot: "$$ROOT" } }; 这意味着单独替换文档是没有意义的。如果要替换$addFields它的别名,$setupdateMany这是别名之一update,那么你得到完全相同的答案,这个上面。
Xavier Guihot
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.