Comment mettre à jour partiellement un objet dans MongoDB pour que le nouvel objet se superpose / fusionne avec l'existant

183

Compte tenu de ce document enregistré dans MongoDB

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
   }
}

Un objet contenant de nouvelles informations sur param2et en param3provenance du monde extérieur doit être enregistré

var new_info = {
    param2 : "val2_new",
    param3 : "val3_new"
};

Je veux fusionner / superposer les nouveaux champs sur l'état existant de l'objet afin que param1 ne soit pas supprimé

Ce faisant

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

Mènera à MongoDB fait exactement comme il a été demandé, et définit some_key sur cette valeur. remplacer l'ancien.

{
   _id : ...,
   some_key: { 
      param2 : "val2_new",
      param3 : "val3_new"
   }
}

Comment faire pour que MongoDB ne mette à jour que les nouveaux champs (sans les déclarer explicitement un par un)? pour obtenir ceci:

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2_new",
        param3 : "val3_new"
   }
}

J'utilise le client Java, mais tout exemple sera apprécié

Eran Medan
la source
2
Veuillez voter pour le numéro de $ merge: jira.mongodb.org/browse/SERVER-21094
Ali

Réponses:

107

Si je comprends bien la question, vous souhaitez mettre à jour un document avec le contenu d'un autre document, mais uniquement les champs qui ne sont pas déjà présents, et ignorer complètement les champs qui sont déjà définis (même s'ils ont une autre valeur).

Il n'y a aucun moyen de faire cela en une seule commande.

Vous devez d'abord interroger le document, déterminer ce que vous voulez $set, puis le mettre à jour (en utilisant les anciennes valeurs comme filtre correspondant pour vous assurer de ne pas obtenir de mises à jour simultanées entre les deux).


Une autre lecture de votre question serait que vous êtes satisfait $set, mais que vous ne voulez pas définir explicitement tous les champs. Comment transmettriez-vous les données alors?

Vous savez que vous pouvez faire ce qui suit:

db.collection.update(  { _id:...} , { $set: someObjectWithNewData } 
Thilo
la source
Si vous souhaitez définir tous les champs sans condition, vous pouvez utiliser le paramètre $ multi , comme ceci: db.collection.update( { _id:...} , { $set: someObjectWithNewData, { $multi: true } } )
Villager
12
Il n'y a pas de paramètre $ multi. Il existe une option multiple (à laquelle vous vous êtes lié), mais cela n'affecte pas la façon dont les champs sont mis à jour. Il contrôle si vous mettez à jour un seul document ou tous les documents qui correspondent au paramètre de requête.
Bob Kerns
1
Cela n'a aucun sens. Lorsqu'il s'agit de mongo, il est également préférable que les mises à jour aient des opérations atomiques. La première lecture provoque une lecture incorrecte chaque fois que quelqu'un d'autre modifie vos données. Et comme mongo n'a pas de transactions, il corrompra volontiers les données pour vous.
froginvasion
@froginvasion: Vous pouvez la rendre atomique en utilisant les anciennes valeurs comme filtre correspondant sur la mise à jour. De cette façon, votre mise à jour échouera s'il y avait une mise à jour concurrente conflictuelle entre-temps. Vous pouvez alors le détecter et agir en conséquence. C'est ce qu'on appelle le verrouillage optimiste. Mais oui, cela dépend de votre application qui l'implémente correctement, Mongo lui-même n'a pas de transactions intégrées.
Thilo
Je pense que c'est une question plus générale de fusion de deux objets en javascript. IIRC l'idée est d'assigner simplement les propriétés du nouveau à l'ancien
information_interchange
110

Je l'ai résolu avec ma propre fonction. Si vous souhaitez mettre à jour le champ spécifié dans le document, vous devez le traiter clairement.

Exemple:

{
    _id : ...,
    some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
    }
}

Si vous ne souhaitez mettre à jour que param2, vous ne devez pas le faire:

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } }  //WRONG

Tu dois utiliser:

db.collection.update(  { _id:...} , { $set: { some_key.param2 : new_info  } } 

Alors j'ai écrit une fonction quelque chose comme ça:

function _update($id, $data, $options=array()){

    $temp = array();
    foreach($data as $key => $value)
    {
        $temp["some_key.".$key] = $value;
    } 

    $collection->update(
        array('_id' => $id),
        array('$set' => $temp)
    );

}

_update('1', array('param2' => 'some data'));
mehmatrix
la source
11
Cela devrait être la réponse acceptée. Pour une meilleure fonction flattenObject, voir gist.github.com/penguinboy/762197
steph643
Merci votre réponse m'a aidé à me guider. J'ai pu modifier la valeur d'une clé spécifique d'un utilisateur dans la collection des utilisateurs comme suit:db.users.update( { _id: ObjectId("594dbc3186bb5c84442949b1") }, { $set: { resume: { url: "http://i.imgur.com/UFX0yXs.jpg" } } } )
Luke Schoen
Cette opération est-elle atomique et isolée? Cela signifie que si 2 threads parallèles essaient de définir 2 champs différents du même document, cela entraînera une condition de
so-random-dude
21

Vous pouvez utiliser la notation par points pour accéder et définir des champs au cœur des objets, sans affecter les autres propriétés de ces objets.

Compte tenu de l'objet que vous avez spécifié ci-dessus:

> db.test.insert({"id": "test_object", "some_key": {"param1": "val1", "param2": "val2", "param3": "val3"}})
WriteResult({ "nInserted" : 1 })

Nous pouvons mettre à jour juste some_key.param2et some_key.param3:

> db.test.findAndModify({
... query: {"id": "test_object"},
... update: {"$set": {"some_key.param2": "val2_new", "some_key.param3": "val3_new"}},
... new: true
... })
{
    "_id" : ObjectId("56476e04e5f19d86ece5b81d"),
    "id" : "test_object",
    "some_key" : {
        "param1" : "val1",
        "param2" : "val2_new",
        "param3" : "val3_new"
    }
}

Vous pouvez plonger aussi profondément que vous le souhaitez. Ceci est également utile pour ajouter de nouvelles propriétés à un objet sans affecter celles existantes.

Samir Talwar
la source
Puis-je ajouter une nouvelle clé ... comme "some key": {"param1": "val1", "param2": "val2_new", "param3": "val3_new", "param4": "val4_new",}
Urvashi Soni
17

La meilleure solution consiste à extraire les propriétés de l'objet et à en faire des paires clé-valeur à notation par points plates. Vous pouvez utiliser par exemple cette bibliothèque:

https://www.npmjs.com/package/mongo-dot-notation

Il a une .flattenfonction qui vous permet de changer l'objet en un ensemble plat de propriétés qui pourraient ensuite être attribuées à $ set modifier, sans craindre que toute propriété de votre objet DB existant soit supprimée / écrasée sans besoin.

Tiré de la mongo-dot-notationdocumentation:

var person = {
  firstName: 'John',
  lastName: 'Doe',
  address: {
    city: 'NY',
    street: 'Eighth Avenu',
    number: 123
  }
};



var instructions = dot.flatten(person)
console.log(instructions);
/* 
{
  $set: {
    'firstName': 'John',
    'lastName': 'Doe',
    'address.city': 'NY',
    'address.street': 'Eighth Avenu',
    'address.number': 123
  }
}
*/

Et puis il forme un sélecteur parfait - il ne mettra à jour QUE les propriétés données. EDIT: j'aime être archéologue parfois;)

SzybkiSasza
la source
6

J'ai eu du succès en le faisant de cette façon:

db.collection.update(  { _id:...} , { $set: { 'key.another_key' : new_info  } } );

J'ai une fonction qui gère dynamiquement les mises à jour de mon profil

function update(prop, newVal) {
  const str = `profile.${prop}`;
  db.collection.update( { _id:...}, { $set: { [str]: newVal } } );
}

Remarque: 'profile' est spécifique à mon implémentation, c'est juste la chaîne de la clé que vous souhaitez modifier.

Matt Smith
la source
1

Il semble que vous puissiez définir isPartialObject qui peut accomplir ce que vous voulez.

Patrick Tescher
la source
Essayé. il ne vous permet pas de sauvegarder des objets partiels si je ne me trompe pas ... mais merci
Eran Medan
1
    // where clause DBObject
    DBObject query = new BasicDBObject("_id", new ObjectId(id));

    // modifications to be applied
    DBObject update = new BasicDBObject();

    // set new values
    update.put("$set", new BasicDBObject("param2","value2"));

   // update the document
    collection.update(query, update, true, false); //3rd param->upsertFlag, 4th param->updateMultiFlag

Si vous avez plusieurs champs à mettre à jour

        Document doc = new Document();
        doc.put("param2","value2");
        doc.put("param3","value3");
        update.put("$set", doc);
Vino
la source
1

À partir de Mongo 4.2, db.collection.update()peut accepter un pipeline d'agrégation, qui permet d'utiliser des opérateurs d'agrégation tels que $addFields, qui sort tous les champs existants à partir des documents d'entrée et des champs nouvellement ajoutés:

var new_info = { param2: "val2_new", param3: "val3_new" }

// { some_key: { param1: "val1", param2: "val2", param3: "val3" } }
// { some_key: { param1: "val1", param2: "val2"                 } }
db.collection.update({}, [{ $addFields: { some_key: new_info } }], { multi: true })
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
  • La première partie {}est la requête de correspondance, filtrant les documents à mettre à jour (dans ce cas, tous les documents).

  • La deuxième partie [{ $addFields: { some_key: new_info } }]est le pipeline d'agrégation de mise à jour:

    • Notez les crochets carrés signifiant l'utilisation d'un pipeline d'agrégation.
    • Puisqu'il s'agit d'un pipeline d'agrégation, nous pouvons utiliser $addFields.
    • $addFields effectue exactement ce dont vous avez besoin: mettre à jour l'objet pour que le nouvel objet se superpose / fusionne avec l'existant:
    • Dans ce cas, { param2: "val2_new", param3: "val3_new" }sera fusionné avec l'existant some_keyen le gardant param1intact et en ajoutant ou en remplaçant les deux param2et param3.
  • N'oubliez pas { multi: true }, sinon seul le premier document correspondant sera mis à jour.

Xavier Guihot
la source
1
db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

à

db.collection.update( { _id: ..} , { $set: { some_key: { param1: newValue} } } ); 

J'espère que cette aide!

nguyenthang338
la source
1

Vous pouvez plutôt faire un upsert, cette opération dans MongoDB est utilisée pour enregistrer le document dans la collection. Si le document correspond aux critères de requête, il effectuera une opération de mise à jour, sinon il insérera un nouveau document dans la collection.

quelque chose de similaire comme ci-dessous

db.employees.update(
    {type:"FT"},
    {$set:{salary:200000}},
    {upsert : true}
 )
Pravin
la source
0

utilisez $ set pour faire ce processus

.update({"_id": args.dashboardId, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
.exec()
.then(dashboardDoc => {
    return {
        result: dashboardDoc
    };
}); 
KARTHIKEYAN.A
la source
0

Créez un objet de mise à jour avec les noms de propriété, y compris le chemin de point nécessaire. ("somekey." + de l'exemple d'OP), puis utilisez-le pour effectuer la mise à jour.

//the modification that's requested
updateReq = { 
   param2 : "val2_new",
   param3 : "val3_new"   
}

//build a renamed version of the update request
var update = {};
for(var field in updateReq){
    update["somekey."+field] = updateReq[field];
}

//apply the update without modifying fields not originally in the update
db.collection.update({._id:...},{$set:update},{upsert:true},function(err,result){...});
Andrew
la source
0

Oui, le meilleur moyen est de convertir la notation d'objet en une représentation de chaîne clé-valeur plate, comme mentionné dans ce commentaire: https://stackoverflow.com/a/39357531/2529199

Je voulais mettre en évidence une méthode alternative utilisant cette bibliothèque NPM: https://www.npmjs.com/package/dot-object qui vous permet de manipuler différents objets en utilisant la notation par points.

J'ai utilisé ce modèle pour créer par programme une propriété d'objet imbriquée lors de l'acceptation de la valeur-clé en tant que variable de fonction, comme suit:

const dot = require('dot-object');

function(docid, varname, varvalue){
  let doc = dot.dot({
      [varname]: varvalue 
  });

  Mongo.update({_id:docid},{$set:doc});
}

Ce modèle me permet d'utiliser indifféremment les propriétés imbriquées et à un seul niveau et de les insérer proprement dans Mongo.

Si vous avez besoin de jouer avec des objets JS au-delà de Mongo, en particulier du côté client, mais que vous êtes cohérent lorsque vous travaillez avec Mongo, cette bibliothèque vous offre plus d'options que le mongo-dot-notationmodule NPM mentionné précédemment .

PS Je voulais à l'origine mentionner cela comme un commentaire, mais apparemment, mon représentant S / O n'est pas assez élevé pour publier un commentaire. Donc, n'essayez pas de vous muscler sur le commentaire de SzybkiSasza, je voulais juste souligner la fourniture d'un module alternatif.

Ashwin Shankar
la source
0

Vous devriez penser à mettre à jour l'objet de manière interchangeable, puis simplement stocker l'objet avec les champs mis à jour. Quelque chose comme fait ci-dessous

function update(_id) {
    return new Promise((resolve, reject) => {

        ObjModel.findOne({_id}).exec((err, obj) => {

            if(err) return reject(err)

            obj = updateObject(obj, { 
                some_key: {
                    param2 : "val2_new",
                    param3 : "val3_new"
                }
            })

            obj.save((err, obj) => {
                if(err) return reject(err)
                resolve(obj)
            })
        })
    })
}


function updateObject(obj, data) {

    let keys = Object.keys(data)

    keys.forEach(key => {

        if(!obj[key]) obj[key] = data[key]

        if(typeof data[key] == 'object') 
            obj[key] = updateObject(obj[key], data[key])
        else
            obj[key] = data[key]
    })

    return obj
}
Ivan Bravo Mendoza
la source
0

Vous devez utiliser des documents incorporés (stringfy l'objet path)

let update = {}
Object.getOwnPropertyNames(new_info).forEach(param => {
   update['some_key.' + param] = new_info[param]
})

Et donc, en JavaScript, vous pouvez utiliser Spread Operator (...) pour mettre à jour

db.collection.update(  { _id:...} , { $set: { ...update  } } 
Guihgo
la source