Perbarui bidang MongoDB menggunakan nilai bidang lain

372

Di MongoDB, apakah mungkin untuk memperbarui nilai bidang menggunakan nilai dari bidang lain? SQL yang setara akan menjadi seperti:

UPDATE Person SET Name = FirstName + ' ' + LastName

Dan pseudo-code MongoDB adalah:

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

Jawaban:

260

Cara terbaik untuk melakukan ini adalah dalam versi 4.2+ yang memungkinkan penggunaan pipa agregasi dalam dokumen pembaruan dan updateOne, updateManyatau updatemetode pengumpulan. Perhatikan bahwa yang terakhir telah usang di sebagian besar jika tidak semua driver bahasa.

MongoDB 4.2+

Versi 4.2 juga memperkenalkan $setoperator tahap pipa yang merupakan alias untuk $addFields. Saya akan gunakan di $setsini karena memetakan dengan apa yang kita coba capai.

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

MongoDB 3.4+

Dalam 3.4+ Anda dapat menggunakan $addFieldsdan $outoperator pipa agregasi.

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

Perhatikan bahwa ini tidak memperbarui koleksi Anda tetapi ganti koleksi yang ada atau buat yang baru. Juga untuk memperbarui operasi yang memerlukan "ketik casting" Anda akan memerlukan pemrosesan sisi klien, dan tergantung pada operasi, Anda mungkin perlu menggunakan find()metode ini daripada.aggreate() metode.

MongoDB 3.2 dan 3.0

Cara kami melakukan ini adalah dengan $projectmemasukkan dokumen kami dan menggunakan $concatoperator agregasi string untuk mengembalikan string yang digabungkan. we Dari sana, Anda lalu iterasi kursor dan gunakan $setoperator pembaruan untuk menambahkan bidang baru ke dokumen Anda menggunakan operasi massal untuk efisiensi maksimum.

Permintaan agregasi:

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

MongoDB 3.2 atau lebih baru

dari ini, Anda perlu menggunakan bulkWritemetode ini.

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 dan 3.0

Dari versi ini Anda perlu menggunakan BulkAPI yang sekarang sudah tidak digunakan lagi dan metode terkaitnya .

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 } }
    );
})
styvane
sumber
Saya pikir ada masalah dengan kode untuk "MongoDB 3.2 atau yang lebih baru". Karena forEach adalah async, biasanya tidak ada yang ditulis dalam bulkWrite terakhir.
Viktor Hedefalk
3
4.2+ Tidak berfungsi. MongoError: Bidang awalan dolar ($) '$ concat' di 'name. $ Concat' tidak valid untuk penyimpanan.
Josh Woodcock
@ JoshWoodcock, saya pikir Anda salah ketik dalam kueri yang Anda jalankan. Saya sarankan Anda periksa.
styvane
@JoshWoodcock Ini berfungsi dengan indah. Silakan uji ini menggunakan MongoDB Web Shell
styvane
2
Bagi mereka yang mengalami masalah yang sama @JoshWoodcock dijelaskan: perhatikan bahwa jawaban untuk 4.2+ menggambarkan pipa agregasi , jadi jangan lewatkan tanda kurung siku di parameter kedua!
philsch
240

Anda harus mengulanginya. Untuk kasus spesifik Anda:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Carlos Barcelona
sumber
4
Apa yang terjadi jika pengguna lain telah mengubah dokumen antara find () dan save () Anda?
UpTheCreek
3
Benar, tetapi menyalin antar bidang seharusnya tidak mengharuskan transaksi bersifat atomik.
UpTheCreek
3
Sangat penting untuk diperhatikan yang save()sepenuhnya menggantikan dokumen. Sebaiknya gunakan update()saja.
Carlos
12
Bagaimanadb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas
1
Saya membuat fungsi yang disebut create_guidyang hanya menghasilkan panduan unik per dokumen ketika iterasi dengan forEachcara ini (yaitu hanya menggunakan create_guiddalam updatepernyataan yang mutli=truemenyebabkan panduan yang sama dihasilkan untuk semua dokumen). Jawaban ini sangat cocok untuk saya. +1
rmirabelle
103

Rupanya ada cara untuk melakukan ini secara efisien sejak MongoDB 3.4, lihat jawaban styvane .


Jawaban usang di bawah

Anda tidak dapat merujuk ke dokumen itu sendiri dalam pembaruan (belum). Anda harus mengulang melalui dokumen dan memperbarui setiap dokumen menggunakan fungsi. Lihat jawaban ini sebagai contoh, atau ini untuk sisi server eval().

Niels van der Rest
sumber
31
Apakah ini masih berlaku hari ini?
Christian Engel
3
@ChristianEngel: Sepertinya begitu. Saya tidak dapat menemukan apa pun di dokumen MongoDB yang menyebutkan referensi ke dokumen saat ini dalam updateoperasi. Permintaan fitur terkait ini juga masih belum terselesaikan.
Niels van der Rest
4
Apakah masih berlaku di bulan April 2017? Atau sudah ada fitur baru yang bisa melakukan ini?
Kim
1
@ Kim Sepertinya masih valid. Selain itu, permintaan fitur yang ditunjukkan @ niels-van-der-rest pada tahun 2013 masih masuk OPEN.
Danziger
8
ini bukan jawaban yang valid lagi, lihat jawaban
@styvane
45

Untuk database dengan aktivitas tinggi, Anda dapat mengalami masalah di mana pembaruan Anda memengaruhi perubahan catatan yang aktif dan untuk alasan ini saya sarankan menggunakan 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/

Eric Kigathi
sumber
2
Apa yang terjadi jika pengguna lain mengedit orang tersebut di antara find () dan save ()? Saya memiliki kasus di mana beberapa panggilan dapat dilakukan ke objek yang sama mengubahnya berdasarkan nilai saat ini. Pengguna ke-2 harus menunggu dengan membaca sampai tanggal 1 selesai dengan penyimpanan. Apakah ini berhasil?
Marco
4
Tentang 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. tautan
ppython
10

Mengenai jawaban ini , fungsi foto tidak digunakan lagi dalam versi 3.6, sesuai dengan pembaruan ini . Jadi, pada versi 3.6 dan di atasnya, dimungkinkan untuk melakukan operasi dengan cara ini:

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

Mulai Mongo 4.2, db.collection.update()dapat menerima pipa agregasi, akhirnya memungkinkan pembaruan / pembuatan bidang berdasarkan bidang lain:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • Bagian pertama {}adalah kueri kecocokan, memfilter dokumen mana yang akan diperbarui (dalam kasus kami semua dokumen).

  • Bagian kedua [{ $set: { name: { ... } }]adalah pipa agregasi pembaruan (perhatikan tanda kurung kotak menandakan penggunaan pipa agregasi). $setadalah operator agregasi baru dan alias $addFields.

  • Jangan lupa { multi: true }, jika tidak , hanya dokumen yang cocok pertama yang akan diperbarui.

Xavier Guihot
sumber
8

Saya mencoba solusi di atas tetapi saya merasa tidak cocok untuk sejumlah besar data. Saya kemudian menemukan fitur aliran:

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
    })
})
Chris Gibb
sumber
1
Bagaimana ini berbeda? Apakah uap akan dibatasi oleh aktivitas pembaruan? Apakah Anda punya referensi untuk itu? Dokumen Mongo sangat buruk.
Nico
2

Inilah yang kami buat untuk menyalin satu bidang ke bidang lain untuk ~ 150_000 catatan. Butuh sekitar 6 menit, tetapi masih jauh lebih sedikit sumber daya intensif daripada instantiate dan iterate atas jumlah objek ruby ​​yang sama.

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)
Chris Bloom
sumber
1

Dengan MongoDB versi 4.2+ , pembaruan lebih fleksibel karena memungkinkan penggunaan pipa agregasi di dalamnya update, updateOnedan updateMany. Anda sekarang dapat mengubah dokumen Anda menggunakan operator agregasi kemudian memperbarui tanpa perlu menjelaskan status $setperintah (sebagai gantinya kami menggunakan$replaceRoot: {newRoot: "$$ROOT"} )

Di sini kami menggunakan kueri agregat untuk mengekstrak timestamp dari bidang ObjectID "_id" MongoDB dan memperbarui dokumen (Saya bukan ahli dalam SQL tapi saya pikir SQL tidak menyediakan ObjectID yang dibuat secara otomatis yang memiliki timestamp untuk itu, Anda harus secara otomatis membuat tanggal itu)

var collection = "person"

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

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
sumber
Anda tidak perlu { $replaceRoot: { newRoot: "$$ROOT" } }; itu berarti mengganti dokumen dengan sendirinya, yang tidak ada gunanya. Jika Anda mengganti $addFieldsdengan alias-nya $setdan updateManyyang merupakan salah satu alias untuk update, maka Anda mendapatkan jawaban yang sama persis seperti yang satu ini di atas.
Xavier Guihot