Mettre à jour le champ MongoDB en utilisant la valeur d'un autre champ

372

Dans MongoDB, est-il possible de mettre à jour la valeur d'un champ en utilisant la valeur d'un autre champ? Le SQL équivalent serait quelque chose comme:

UPDATE Person SET Name = FirstName + ' ' + LastName

Et le pseudo-code MongoDB serait:

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

Réponses:

260

La meilleure façon de le faire est en version 4.2+ qui permet l' utilisation d' un pipeline d'agrégation dans le document de mise à jour et updateOne, updateManyou la updateméthode de collecte. Notez que ce dernier est obsolète dans la plupart des pilotes de langues, sinon dans toutes.

MongoDB 4.2+

La version 4.2 a également introduit l' $setopérateur d'étape de pipeline qui est un alias pour $addFields. Je vais l'utiliser $setici car il correspond à ce que nous essayons de réaliser.

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

MongoDB 3.4+

Dans la version 3.4+, vous pouvez utiliser $addFieldsles $outopérateurs de pipeline d'agrégation.

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

Notez que cela ne met pas à jour votre collection mais remplace plutôt la collection existante ou en crée une nouvelle. De plus, pour les opérations de mise à jour qui nécessitent une «conversion de type», vous aurez besoin d'un traitement côté client et, selon l'opération, vous devrez peut-être utiliser la find()méthode au lieu de la .aggreate()méthode.

MongoDB 3.2 et 3.0

Pour ce faire, nous utilisons $projectnos documents et utilisons l' $concatopérateur d'agrégation de chaînes pour renvoyer la chaîne concaténée. À partir de là, vous itérez ensuite le curseur et utilisez l' $setopérateur de mise à jour pour ajouter le nouveau champ à vos documents à l'aide d' opérations en bloc pour une efficacité maximale.

Requête d'agrégation:

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

MongoDB 3.2 ou plus récent

à partir de cela, vous devez utiliser la bulkWriteméthode.

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

À partir de cette version, vous devez utiliser l' BulkAPI désormais obsolète et ses méthodes associées .

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
la source
Je pense qu'il y a un problème avec le code de "MongoDB 3.2 ou plus récent". Étant donné que forEach est asynchrone, rien ne sera généralement écrit dans le dernier bulkWrite.
Viktor Hedefalk
3
4.2+ Ne fonctionne pas. MongoError: Le champ préfixé dollar ($) '$ concat' dans 'nom. $ Concat' n'est pas valide pour le stockage.
Josh Woodcock
@JoshWoodcock, je pense que vous aviez une faute de frappe dans la requête que vous exécutez. Je vous suggère de revérifier.
styvane
@JoshWoodcock Cela fonctionne à merveille. Veuillez tester cela en utilisant le MongoDB Web Shell
styvane
2
Pour ceux qui rencontrent le même problème que @JoshWoodcock a décrit: faites attention que la réponse pour 4.2+ décrit un pipeline d'agrégation , alors ne manquez pas les crochets dans le deuxième paramètre!
philsch
240

Vous devriez parcourir. Pour votre cas spécifique:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Carlos Barcelona
la source
4
Que se passe-t-il si un autre utilisateur a changé le document entre votre find () et votre save ()?
UpTheCreek
3
Certes, mais la copie entre les champs ne doit pas exiger que les transactions soient atomiques.
UpTheCreek
3
Il est important de noter que save()remplace complètement le document. Devrait utiliser à la update()place.
Carlos
12
Que diriez-vousdb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas
1
J'ai créé une fonction appelée create_guidqui ne produisait un GUID unique par document que lors de l'itération de forEachcette manière (c.-à-d. Que l'utilisation create_guidd'une updateinstruction avec mutli=truele même GUID était générée pour tous les documents). Cette réponse a parfaitement fonctionné pour moi. +1
rmirabelle
103

Apparemment, il existe un moyen de le faire efficacement depuis MongoDB 3.4, voir la réponse de styvane .


Réponse obsolète ci-dessous

Vous ne pouvez pas (encore) faire référence au document lui-même dans une mise à jour. Vous devrez parcourir les documents et mettre à jour chaque document à l'aide d'une fonction. Voir cette réponse pour un exemple, ou celle-ci pour le côté serveur eval().

Niels van der Rest
la source
31
Est-ce toujours valable aujourd'hui?
Christian Engel
3
@ChristianEngel: Il semble que oui. Je n'ai pas pu trouver quoi que ce soit dans les documents MongoDB qui mentionne une référence au document actuel dans une updateopération. Cette demande de fonctionnalité associée n'est toujours pas résolue également.
Niels van der Rest le
4
Est-il toujours valable en avril 2017? Ou il existe déjà de nouvelles fonctionnalités qui peuvent le faire?
Kim
1
@Kim Il semble qu'il soit toujours valide. De plus, la demande de fonctionnalité que @ niels-van-der-rest a soulignée en 2013 est toujours en cours OPEN.
Danziger
8
ce n'est plus une réponse valide, jetez un œil à la réponse
@styvane
45

Pour une base de données avec une activité élevée, vous pouvez rencontrer des problèmes où vos mises à jour affectent activement les enregistrements changeants et pour cette raison, je recommande d'utiliser 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
la source
2
Que se passe-t-il si un autre utilisateur modifie la personne entre find () et save ()? J'ai un cas où plusieurs appels peuvent être effectués vers le même objet en les changeant en fonction de leurs valeurs actuelles. Le 2e utilisateur doit attendre la lecture jusqu'à ce que le 1er ait terminé la sauvegarde. Est-ce que cela accomplit cela?
Marco
4
À propos de 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. lien
ppython
10

Concernant cette réponse , la fonction snapshot est déconseillée dans la version 3.6, selon cette mise à jour . Ainsi, sur la version 3.6 et supérieure, il est possible d'effectuer l'opération de cette façon:

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

Au départ Mongo 4.2, db.collection.update()peut accepter un pipeline d'agrégation, permettant enfin la mise à jour / création d'un champ basé sur un autre champ:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • La première partie {}est la requête de correspondance, filtrant les documents à mettre à jour (dans notre cas tous les documents).

  • La deuxième partie [{ $set: { name: { ... } }]est le pipeline d'agrégation de mise à jour (notez les crochets au carré signifiant l'utilisation d'un pipeline d'agrégation). $setest un nouvel opérateur d'agrégation et un alias de $addFields.

  • N'oubliez pas { multi: true }, sinon seul le premier document correspondant sera mis à jour.

Xavier Guihot
la source
8

J'ai essayé la solution ci-dessus mais je l'ai trouvée inappropriée pour de grandes quantités de données. J'ai ensuite découvert la fonctionnalité de flux:

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
la source
1
En quoi est-ce différent? La vapeur sera-t-elle étranglée par l'activité de mise à jour? En avez-vous une référence? Les documents Mongo sont assez pauvres.
Nico
2

Voici ce que nous avons trouvé pour copier un champ dans un autre pour ~ 150_000 enregistrements. Cela a pris environ 6 minutes, mais est toujours beaucoup moins gourmand en ressources qu'il ne l'aurait été pour instancier et itérer sur le même nombre d'objets rubis.

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
la source
1

Avec MongoDB version 4.2+ , les mises à jour sont plus flexibles car elles permettent d'utiliser le pipeline d'agrégation dans ses update, updateOneet updateMany. Vous pouvez maintenant transformer vos documents en utilisant les opérateurs d'agrégation puis les mettre à jour sans avoir besoin d'expliciter la $setcommande (à la place nous utilisons $replaceRoot: {newRoot: "$$ROOT"})

Ici, nous utilisons la requête agrégée pour extraire l'horodatage du champ ObjectID "_id" de MongoDB et mettre à jour les documents (je ne suis pas un expert en SQL, mais je pense que SQL ne fournit aucun ObjectID généré automatiquement avec horodatage, vous devez créer automatiquement cette date)

var collection = "person"

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

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
la source
Vous n'avez pas besoin { $replaceRoot: { newRoot: "$$ROOT" } }; cela signifie remplacer le document par lui-même, ce qui est inutile. Si vous remplacez $addFieldspar son alias $setet updateManyqui est l'un des alias de update, alors vous obtenez exactement la même réponse que celle ci- dessus.
Xavier Guihot