MongoDB - Mettre à jour des objets dans le tableau d'un document (mise à jour imbriquée)

204

Supposons que nous ayons la collection suivante, sur laquelle j'ai quelques questions:

{
    "_id" : ObjectId("4faaba123412d654fe83hg876"),
    "user_id" : 123456,
    "total" : 100,
    "items" : [
            {
                    "item_name" : "my_item_one",
                    "price" : 20
            },
            {
                    "item_name" : "my_item_two",
                    "price" : 50
            },
            {
                    "item_name" : "my_item_three",
                    "price" : 30
            }
    ]
}

1 - Je souhaite augmenter le prix de "item_name": "my_item_two" et s'il n'existe pas , il doit être ajouté au tableau "items".

2 - Comment mettre à jour deux champs en même temps. Par exemple, augmentez le prix de "my_item_three" et en même temps augmentez le "total" (avec la même valeur).

Je préfère le faire du côté de MongoDB, sinon je dois charger le document côté client (Python) et construire le document mis à jour et le remplacer par celui existant dans MongoDB.

MISE À JOUR Voici ce que j'ai essayé et qui fonctionne très bien SI l'objet existe :

db.test_invoice.update({user_id : 123456 , "items.item_name":"my_item_one"} , {$inc: {"items.$.price": 10}})

Mais si la clé n'existe pas, elle ne fait rien. De plus, il ne met à jour que l'objet imbriqué. Il n'y a aucun moyen avec cette commande de mettre à jour le champ "total" également.

Majid
la source
1
Je pense que vous ne pouvez pas faire cela en mongo, sauf peut-être avec beaucoup de douleur en utilisant l'eval. Mongo est très limité dans les opérations de données.
Antti Haapala
3
@Haapala: mongodb a $ inc et mise à jour avec upsert
jdi
1
@jdi oui oui, mais cela n'aide pas beaucoup ici, mais ce dont il a besoin est de plusieurs $ incs, conditionnellement, et si l'article n'existe pas, alors $ push est nécessaire.
Antti Haapala

Réponses:

237

Pour la question n ° 1, divisons-la en deux parties. Tout d'abord, incrémentez tout document qui a "items.item_name" égal à "my_item_two". Pour cela, vous devrez utiliser l'opérateur positionnel "$". Quelque chose comme:

 db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Notez que cela n'incrémentera que le premier sous-document correspondant dans n'importe quel tableau (donc si vous avez un autre document dans le tableau avec "item_name" égal à "my_item_two", il ne sera pas incrémenté). Mais c'est peut-être ce que vous voulez.

La deuxième partie est plus délicate. Nous pouvons pousser un nouvel élément vers un tableau sans "my_item_two" comme suit:

 db.bar.update( {user_id : 123456, "items.item_name" : {$ne : "my_item_two" }} , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

Pour votre question n ° 2, la réponse est plus simple. Pour incrémenter le total et le prix de item_three dans tout document contenant "my_item_three", vous pouvez utiliser l'opérateur $ inc sur plusieurs champs en même temps. Quelque chose comme:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);
matulef
la source
Merci pour la réponse. La réponse à la question n ° 2 est parfaite et fonctionne bien :) Mais pour la question n ° 1, le problème est que vous devez rechercher le document par "user_id", et non par "items.item_name". Dans ce cas, je ne peux pas utiliser l'opérateur positionnel, car je n'ai pas spécifié le tableau dans la requête de recherche. Je veux aussi que les deux parties soient faites en un seul coup. (si possible)
Majid
De beaux exemples. J'avais l'impression de rendre cette direction évidente à partir des liens dans ma réponse, mais je n'arrêtais pas de résister en disant que ce n'était pas ce qu'il cherchait. Je suppose que l'OP avait juste besoin de voir des exemples sur la façon de faire plusieurs $ inc.
jdi
Pour la question n ° 1, vous devriez toujours pouvoir le faire en deux parties en ajoutant le user_id au bit de requête de chacune des mises à jour (je modifierai la réponse en conséquence). Devra réfléchir davantage à savoir s'il est possible de le faire en un seul coup.
matulef
6
Quels sont les 3e et 4e paramètres de la méthode de mise à jour? et pourquoi?
skmahawar
5
@skmahawar concernant les 3e et 4e paramètres, docs.mongodb.com/manual/reference/method/db.collection.update indique que ces options sont respectivement pour "upsert" et "multi". Pour upsert, s'il est défini sur true, crée un nouveau document lorsqu'aucun document ne correspond aux critères de requête. La valeur par défaut est false, qui n'insère pas de nouveau document lorsqu'aucune correspondance n'est trouvée. "Pour multi, si la valeur est true, met à jour plusieurs documents qui répondent aux critères de requête. Si la valeur est false, met à jour un document. La valeur par défaut est faux.
Mike Strand
24

Il n'y a aucun moyen de le faire en une seule requête. Vous devez rechercher le document dans la première requête:

Si le document existe:

db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Autre

db.bar.update( {user_id : 123456 } , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

Pas besoin d'ajouter de condition {$ne : "my_item_two" } .

De plus, dans un environnement multithread, vous devez faire attention à ce qu'un seul thread puisse exécuter le second (insérer la casse, si le document n'a pas été trouvé) à la fois, sinon des documents incorporés en double seront insérés.

Udit
la source
1
non, il existe un moyen de le faire en une seule requête, voir ci
Martijn Scheffer
1
@MartijnScheffer, je ne vois pas de moyen de le faire comme une seule requête n'importe où dans ce fil. Même la "réponse" utilise 2 requêtes identiques à celles de cette réponse. Suis-je en train de manquer quelque chose? J'espère également qu'il existe un moyen de le faire dans une seule requête pour éviter tout problème de multi-threading
dferraro
@Udit, comment puis-je utiliser les articles. $. Prix dans la condition? lorsque j'essaie d'ajouter une condition telle que "incrémenter uniquement si le prix est supérieur à 0", cela ne fonctionne pas - en gros, rien n'est mis à jour. Puis-je l'utiliser dans l'état où, ou manque-t-il quelque chose? articles. $. prix: {$ gt: 0}
dferraro
quelque chose doit avoir disparu :)
Martijn Scheffer
3

Nous pouvons utiliser l' $setopérateur pour mettre à jour le tableau imbriqué à l'intérieur de l'objet déposé mettre à jour la valeur

db.getCollection('geolocations').update( 
   {
       "_id" : ObjectId("5bd3013ac714ea4959f80115"), 
       "geolocation.country" : "United States of America"
   }, 
   { $set: 
       {
           "geolocation.$.country" : "USA"
       } 
    }, 
   false,
   true
);
KARTHIKEYAN.A
la source