Aktualisieren Sie das MongoDB-Feld mit dem Wert eines anderen Felds

372

Ist es in MongoDB möglich, den Wert eines Feldes mithilfe des Werts aus einem anderen Feld zu aktualisieren? Das äquivalente SQL wäre ungefähr so:

UPDATE Person SET Name = FirstName + ' ' + LastName

Und der MongoDB-Pseudocode wäre:

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

Antworten:

259

Der beste Weg , dies zu tun , ist in der Version 4.2 und höher , die in dem Update - Dokument mit der Aggregation Pipeline ermöglicht und das updateOne, updateManyoder updateSammelverfahren. Beachten Sie, dass Letzteres in den meisten, wenn nicht allen Sprachtreibern veraltet ist.

MongoDB 4.2+

In Version 4.2 wurde auch der $setPipeline-Stage-Operator eingeführt, der ein Alias ​​für ist $addFields. Ich verwende $sethier , wie es Karten mit dem, was wir zu erreichen versuchen.

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

MongoDB 3.4+

In Version 3.4+ können Sie $addFieldsdie $outAggregationspipeline-Operatoren verwenden.

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

Beachten Sie, dass dadurch Ihre Sammlung nicht aktualisiert wird, sondern die vorhandene Sammlung ersetzt oder eine neue erstellt wird. Auch für Aktualisierungsvorgänge, die "Typumwandlung" erfordern, benötigen Sie eine clientseitige Verarbeitung. Je nach Vorgang müssen Sie möglicherweise die find()Methode anstelle der .aggreate()Methode verwenden.

MongoDB 3.2 und 3.0

Dazu verwenden wir $projectunsere Dokumente und verwenden den $concatZeichenfolgenaggregationsoperator, um die verkettete Zeichenfolge zurückzugeben. Von dort aus iterieren Sie dann den Cursor und verwenden den $setAktualisierungsoperator, um das neue Feld mithilfe von Massenoperationen zu Ihren Dokumenten hinzuzufügen, um maximale Effizienz zu erzielen.

Aggregationsabfrage:

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

MongoDB 3.2 oder neuer

Aus diesem Grund müssen Sie die bulkWriteMethode verwenden.

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

Ab dieser Version müssen Sie die jetzt veraltete BulkAPI und die zugehörigen Methoden verwenden .

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
quelle
Ich denke, es gibt ein Problem mit dem Code für "MongoDB 3.2 oder neuer". Da forEach asynchron ist, wird normalerweise nichts im letzten BulkWrite geschrieben.
Viktor Hedefalk
3
4.2+ Funktioniert nicht. MongoError: Das Dollar-Präfixfeld '$ concat' in 'name. $ Concat' ist für die Speicherung nicht gültig.
Josh Woodcock
@ JoshWoodcock, ich denke, Sie hatten einen Tippfehler in der Abfrage, die Sie ausführen. Ich schlage vor, Sie überprüfen.
Styvane
@ JoshWoodcock Es funktioniert wunderbar. Bitte testen Sie dies mit der MongoDB Web Shell
styvane
2
Für diejenigen, die auf dasselbe Problem stoßen, das @JoshWoodcock beschrieben hat: Achten Sie darauf, dass die Antwort für 4.2+ eine Aggregationspipeline beschreibt. Verpassen Sie also nicht die eckigen Klammern im zweiten Parameter!
Philsch
240

Sie sollten durchlaufen. Für Ihren speziellen Fall:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Carlos Barcelona
quelle
4
Was passiert, wenn ein anderer Benutzer das Dokument zwischen find () und save () geändert hat?
UpTheCreek
3
Richtig, aber das Kopieren zwischen Feldern sollte nicht erfordern, dass Transaktionen atomar sind.
UpTheCreek
3
Es ist wichtig zu beachten, dass save()das Dokument vollständig ersetzt wird. Sollte update()stattdessen verwenden.
Carlos
12
Wie wäre esdb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas
1
Ich habe eine Funktion namens aufgerufen create_guid, die nur dann eine eindeutige Guid pro Dokument erzeugt, wenn sie forEachauf diese Weise iteriert wird (dh einfach durch Verwendung create_guideiner updateAnweisung mit mutli=truebewirkt, dass für alle Dokumente dieselbe Guid generiert wird). Diese Antwort hat bei mir perfekt funktioniert. +1
Rmirabelle
103

Anscheinend gibt es seit MongoDB 3.4 eine Möglichkeit, dies effizient zu tun, siehe Styvanes Antwort .


Veraltete Antwort unten

Sie können (noch) nicht auf das Dokument selbst in einem Update verweisen. Sie müssen die Dokumente durchlaufen und jedes Dokument mithilfe einer Funktion aktualisieren. Sehen Sie diese Antwort für ein Beispiel, oder diese eine für die serverseitige eval().

Niels van der Rest
quelle
31
Ist das heute noch gültig?
Christian Engel
3
@ChristianEngel: Es scheint so. Ich konnte in den MongoDB-Dokumenten nichts finden, das einen Verweis auf das aktuelle Dokument in einer updateOperation erwähnt. Diese zugehörige Funktionsanforderung ist ebenfalls noch ungelöst.
Niels van der Rest
4
Ist es noch im April 2017 gültig? Oder gibt es bereits neue Funktionen, die dies können?
Kim
1
@ Kim Es sieht so aus, als ob es noch gültig ist. Auch die Feature-Anfrage , auf die @ niels-van-der-rest bereits 2013 hingewiesen hat, ist noch nicht abgeschlossen OPEN.
Danziger
8
Dies ist keine gültige Antwort mehr. Schauen Sie sich die Antwort von
@styvane
45

Bei einer Datenbank mit hoher Aktivität können Probleme auftreten, bei denen sich Ihre Aktualisierungen auf das aktive Ändern von Datensätzen auswirken. Aus diesem Grund empfehle ich die Verwendung von 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
quelle
2
Was passiert, wenn ein anderer Benutzer die Person zwischen find () und save () bearbeitet hat? Ich habe einen Fall, in dem mehrere Aufrufe an dasselbe Objekt ausgeführt werden können, um sie basierend auf ihren aktuellen Werten zu ändern. Der 2. Benutzer sollte mit dem Lesen warten müssen, bis der 1. mit dem Speichern fertig ist. Schafft das das?
Marco
4
Über die 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. link
ppython
10

In Bezug auf diese Antwort ist die Snapshot-Funktion in Version 3.6 gemäß diesem Update veraltet . Ab Version 3.6 ist es also möglich, den Vorgang folgendermaßen auszuführen:

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

Starten Mongo 4.2, db.collection.update()kann eine Aggregationspipeline akzeptieren und schließlich das Aktualisieren / Erstellen eines Felds basierend auf einem anderen Feld ermöglichen:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • Der erste Teil {}ist die Übereinstimmungsabfrage, bei der gefiltert wird, welche Dokumente aktualisiert werden sollen (in unserem Fall alle Dokumente).

  • Der zweite Teil [{ $set: { name: { ... } }]ist die Update-Aggregationspipeline (beachten Sie die eckigen Klammern, die die Verwendung einer Aggregationspipeline angeben). $setist ein neuer Aggregationsoperator und ein Alias ​​von $addFields.

  • Vergessen Sie nicht { multi: true }, sonst wird nur das erste übereinstimmende Dokument aktualisiert.

Xavier Guihot
quelle
8

Ich habe die obige Lösung ausprobiert, fand sie jedoch für große Datenmengen ungeeignet. Ich habe dann die Stream-Funktion entdeckt:

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
quelle
1
Wie ist das anders? Wird der Dampf durch die Update-Aktivität gedrosselt? Haben Sie einen Hinweis darauf? Die Mongo-Dokumente sind ziemlich arm.
Nico
2

Folgendes haben wir uns ausgedacht, um ein Feld für ~ 150_000 Datensätze in ein anderes zu kopieren. Es dauerte ungefähr 6 Minuten, ist aber immer noch deutlich weniger ressourcenintensiv als das Instanziieren und Iterieren derselben Anzahl von Ruby-Objekten.

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
quelle
1

Mit MongoDB - Version 4.2 und höher , ist Updates flexibler , da es die Verwendung von Aggregation Pipeline in seinem ermöglicht update, updateOneund updateMany. Sie können Ihre Dokumente jetzt mithilfe der Aggregationsoperatoren transformieren und dann aktualisieren, ohne den $setBefehl explizit angeben zu müssen (stattdessen verwenden wir $replaceRoot: {newRoot: "$$ROOT"}).

Hier verwenden wir die aggregierte Abfrage, um den Zeitstempel aus dem Feld ObjectID "_id" von MongoDB zu extrahieren und die Dokumente zu aktualisieren (ich bin kein SQL-Experte, aber ich denke, SQL bietet keine automatisch generierte ObjectID mit Zeitstempel, die Sie benötigen automatisch dieses Datum erstellen)

var collection = "person"

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

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
quelle
Du brauchst nicht { $replaceRoot: { newRoot: "$$ROOT" } }; es bedeutet, das Dokument selbst zu ersetzen, was sinnlos ist. Wenn Sie ersetzen $addFieldsdurch den Alias $setund updateManydie für einen der Aliase ist update, erhalten Sie auf genau die gleiche Antwort wie diese oben.
Xavier Guihot