Cập nhật trường MongoDB bằng cách sử dụng giá trị của trường khác


372

Trong MongoDB, có thể cập nhật giá trị của một trường bằng cách sử dụng giá trị từ một trường khác không? SQL tương đương sẽ là một cái gì đó như:

UPDATE Person SET Name = FirstName + ' ' + LastName

Và mã giả MongoDB sẽ là:

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

Câu trả lời:


259

Cách tốt nhất để làm điều này là trong phiên bản 4.2 trở lên cho phép sử dụng đường ống kết hợp trong tài liệu cập nhật và updateOne, updateManyhoặc updatephương pháp thu thập. Lưu ý rằng phần sau đã bị phản đối trong hầu hết các trình điều khiển ngôn ngữ.

MongoDB 4.2+

Phiên bản 4.2 cũng giới thiệu $settoán tử giai đoạn đường ống là bí danh cho $addFields. Tôi sẽ sử dụng $setở đây vì nó ánh xạ với những gì chúng ta đang cố gắng đạt được.

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

MongoDB 3,4+

Trong 3,4+ bạn có thể sử dụng $addFieldsvà các $outtoán tử đường ống tổng hợp.

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

Lưu ý rằng điều này không cập nhật bộ sưu tập của bạn mà thay vào đó thay thế bộ sưu tập hiện có hoặc tạo bộ sưu tập mới. Ngoài ra, đối với các hoạt động cập nhật yêu cầu "nhập kiểu", bạn sẽ cần xử lý phía máy khách và tùy thuộc vào thao tác, bạn có thể cần sử dụng find()phương thức thay vì .aggreate()phương thức.

MongoDB 3.2 và 3.0

Cách chúng tôi làm điều này là bằng cách nhập $projecttài liệu của chúng tôi và sử dụng $concattoán tử tổng hợp chuỗi để trả về chuỗi được nối. Chúng tôi Từ đó, sau đó bạn lặp lại con trỏ và sử dụng $settoán tử cập nhật để thêm trường mới vào tài liệu của bạn bằng các thao tác hàng loạt để đạt hiệu quả tối đa.

Truy vấn tổng hợp:

var cursor = db.collection.aggregate([ 
    { "$project":  { 
        "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
    }}
])

MongoDB 3.2 trở lên

Từ đây, bạn cần sử dụng bulkWritephương pháp.

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 và 3.0

Từ phiên bản này, bạn cần sử dụng BulkAPI hiện không dùng nữa và các phương thức liên quan .

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

Tôi nghĩ rằng có một vấn đề với mã cho "MongoDB 3.2 hoặc mới hơn". Vì forEach không đồng bộ nên thường sẽ không được ghi trong số lượng lớn Write cuối cùng.
Viktor Hedefalk

3
4.2+ Không hoạt động. MongoError: Trường tiền tố đô la ($) '$ concat' trong 'name. $ Concat' không hợp lệ để lưu trữ.
Josh Woodcock

@JoshWoodcock, tôi nghĩ bạn có một lỗi đánh máy trong truy vấn bạn đang chạy. Tôi đề nghị bạn kiểm tra lại.
styvane

@JoshWoodcock Nó hoạt động rất đẹp. Vui lòng kiểm tra điều này bằng cách sử dụng MongoDB Web Shell
styvane

2
Đối với những người gặp vấn đề tương tự @JoshWoodcock đã mô tả: chú ý rằng câu trả lời cho 4.2+ mô tả một đường ống tổng hợp , vì vậy đừng bỏ lỡ dấu ngoặc vuông trong tham số thứ hai!
philsch

240

Bạn nên lặp qua. Đối với trường hợp cụ thể của bạn:

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

4
Điều gì xảy ra nếu một người dùng khác đã thay đổi tài liệu giữa find () và save () của bạn?
UpTheCux

3
Đúng, nhưng sao chép giữa các trường không nên yêu cầu giao dịch là nguyên tử.
UpTheCalet

3
Điều quan trọng cần lưu ý là save()thay thế hoàn toàn tài liệu. Nên dùng update()thay thế.
Carlos

12
Thế còndb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas

1
Tôi đã tạo một hàm gọi là create_guidchỉ tạo ra một hướng dẫn duy nhất cho mỗi tài liệu khi lặp forEachtheo cách này (nghĩa là chỉ sử dụng create_guidtrong một updatecâu lệnh với nguyên mutli=truenhân tương tự được tạo cho tất cả các tài liệu). Câu trả lời này đã làm việc hoàn hảo cho tôi. +1
rmirabelle

103

Rõ ràng có một cách để làm điều này một cách hiệu quả kể từ MongoDB 3.4, xem câu trả lời của styvane .


Câu trả lời lỗi thời dưới đây

Bạn không thể tham khảo tài liệu trong bản cập nhật (chưa). Bạn sẽ cần lặp lại qua các tài liệu và cập nhật từng tài liệu bằng một hàm. Xem câu trả lời này cho một ví dụ, hoặc câu trả lời này cho phía máy chủ eval().


31
Điều này vẫn còn hiệu lực ngày hôm nay?
Christian Engel

3
@ChristianEngel: Nó xuất hiện như vậy. Tôi không thể tìm thấy bất cứ điều gì trong các tài liệu MongoDB có đề cập đến một tham chiếu đến tài liệu hiện tại trong một updatehoạt động. Yêu cầu tính năng liên quan này vẫn chưa được giải quyết.
Niels van der Nghỉ

4
Nó vẫn còn hiệu lực vào tháng 4 năm 2017? Hoặc đã có các tính năng mới có thể làm điều này?
Kim

1
@Kim Có vẻ như nó vẫn còn hiệu lực. Ngoài ra, yêu cầu tính năng mà @ niels-van-der-rest chỉ ra vào năm 2013 vẫn còn OPEN.
Danziger

8
đây không phải là một câu trả lời hợp lệ nữa, hãy xem câu trả lời
@styvane

45

Đối với cơ sở dữ liệu có hoạt động cao, bạn có thể gặp phải các vấn đề trong đó các cập nhật của bạn ảnh hưởng đến việc thay đổi hồ sơ tích cực và vì lý do này, tôi khuyên bạn nên sử dụng 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/cthon.snapshot/


2
Điều gì xảy ra nếu một người dùng khác chỉnh sửa người giữa find () và save ()? Tôi có một trường hợp trong đó nhiều cuộc gọi có thể được thực hiện cho cùng một đối tượng thay đổi chúng dựa trên các giá trị hiện tại của chúng. Người dùng thứ 2 phải chờ đọc cho đến khi người thứ 1 hoàn thành việc lưu. Điều này có thực hiện được điều đó?
Marco

4
Giới thiệu về 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. liên kết
ppython

10

Liên quan đến câu trả lời này , chức năng chụp nhanh không được dùng trong phiên bản 3.6, theo bản cập nhật này . Vì vậy, trên phiên bản 3.6 trở lên, có thể thực hiện thao tác theo cách này:

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

9

Bắt đầu Mongo 4.2, db.collection.update()có thể chấp nhận một đường ống tổng hợp, cuối cùng cho phép cập nhật / tạo một trường dựa trên một trường khác:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • Phần đầu tiên {}là truy vấn khớp, lọc những tài liệu cần cập nhật (trong trường hợp của chúng tôi là tất cả các tài liệu).

  • Phần thứ hai [{ $set: { name: { ... } }]là đường ống tổng hợp cập nhật (lưu ý dấu ngoặc vuông biểu thị việc sử dụng đường ống tổng hợp). $setlà một toán tử tập hợp mới và bí danh của $addFields.

  • Đừng quên { multi: true }, nếu không thì chỉ có tài liệu phù hợp đầu tiên sẽ được cập nhật.


8

Tôi đã thử giải pháp trên nhưng tôi thấy nó không phù hợp với lượng lớn dữ liệu. Sau đó tôi phát hiện ra tính năng stream:

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
Điều này khác nhau như thế nào? Hơi nước sẽ được điều chỉnh bởi hoạt động cập nhật? Bạn có bất kỳ tài liệu tham khảo cho nó? Các tài liệu Mongo khá nghèo nàn.
Nico

2

Đây là những gì chúng tôi đã đưa ra để sao chép một trường này sang trường khác cho ~ 150_000 hồ sơ. Mất khoảng 6 phút, nhưng vẫn tốn ít tài nguyên hơn đáng kể so với việc khởi tạo và lặp lại trên cùng một số lượng vật thể 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)

1

Với MongoDB phiên bản 4.2 trở lên , cập nhật là linh hoạt hơn vì nó cho phép việc sử dụng các đường ống kết hợp trong nó update, updateOneupdateMany. Bây giờ bạn có thể chuyển đổi tài liệu của mình bằng cách sử dụng các toán tử tổng hợp sau đó cập nhật mà không cần phải giải thích trạng thái $setlệnh (thay vì chúng tôi sử dụng $replaceRoot: {newRoot: "$$ROOT"})

Ở đây chúng tôi sử dụng truy vấn tổng hợp để trích xuất dấu thời gian từ trường "_id" ObjectID của MongoDB và cập nhật các tài liệu (Tôi không phải là chuyên gia về SQL nhưng tôi nghĩ rằng SQL không cung cấp bất kỳ ObjectID nào được tạo tự động có dấu thời gian cho nó, bạn sẽ phải tự động tạo ngày đó)

var collection = "person"

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

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

Bạn không cần { $replaceRoot: { newRoot: "$$ROOT" } }; nó có nghĩa là tự thay thế tài liệu, điều này là vô nghĩa. Nếu bạn thay thế $addFieldsbằng bí danh của mình $setupdateManyđó là một trong những bí danh cho update, sau đó bạn sẽ có được câu trả lời chính xác giống như cái này ở trên.
Xavier Guihot
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.