2012-05-09 7 views
143

Supposons que nous avons la collection suivante, que j'ai quelques questions sur:MongoDB - Mise à jour des objets dans un tableau de documents (mise à jour imbriquée)

{ 
    "_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 veux augmenter le prix pour « item_name »: » my_item_two "et s'il n'existe pas, il doit être ajouté au tableau" items ".

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

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

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

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

Mais si la clé n'existe pas il ne fait rien. De plus, il ne met à jour que l'objet imbriqué. Cette commande ne permet pas non plus de mettre à jour le champ "total".

+1

Je pense que vous ne pouvez pas le faire en mongo, sauf peut-être avec beaucoup de mal en utilisant l'eval. Mongo est très limité dans les opérations de données. –

+3

@Haapala: mongodb a $ inc et mise à jour avec upsert – jdi

+1

@jdi oui oui, mais ça n'aide pas beaucoup ici, mais ce dont il a besoin c'est de multiples $ incs, conditionnellement, et si l'item n'existe pas, alors un push $ est nécessaire. –

Répondre

173

Pour la question 1, divisons-la en deux parties. Commencez par incrémenter tout document dont le nom "items.item_name" est é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émenter le premier sous-document dans un tableau adapté (donc si vous avez un autre document dans le tableau avec « item_name » égal à « my_item_two », il ne sera pas incrémentée) . Mais cela pourrait être ce que vous voulez.

La deuxième partie est plus délicate. Nous pouvons pousser un nouvel élément à 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 facile. 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); 
+0

Merci pour la réponse. La réponse à la question 2 est parfaite et fonctionne bien :) Mais pour la question 1, le problème est que vous devriez rechercher le document par "user_id", et non par "items.item_name". Dans ce cas, je ne peux pas utiliser l'opérateur de position, 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

+0

Bons exemples. J'avais l'impression de faire ressortir cette direction à partir des liens dans ma réponse, mais je n'arrêtais pas d'être de la résistance en disant que ce n'était pas ce qu'il cherchait. Devinez le PO juste besoin de voir des exemples sur la façon de faire plusieurs $ inc. – jdi

+0

Pour la question 1, vous devriez toujours pouvoir le faire en deux parties en ajoutant l'id_utilisateur 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 faire en un seul coup. – matulef

19

Il n'y a aucun moyen de le faire dans 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); 

Else

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

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

En environnement multithread vous devez veiller à ce qu'un seul thread puisse exécuter le second (cas d'insertion si le document n'a pas été trouvé) à la fois, sinon les documents embed dupliqués seront insérés.

+1

non, il existe un moyen de le faire dans une seule requête, voir ci-dessus –

+0

@MartijnScheffer, je ne vois pas un 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. Est-ce que je manque quelque chose? J'espère également qu'il existe un moyen de le faire dans une requête pour éviter les problèmes de multi-threading – dferraro

+0

@Udit, comment puis-je utiliser les éléments. $. Prix dans la condition? Lorsque j'essaie d'ajouter une condition telle que "incrémenter seulement si le prix est supérieur à 0", cela ne fonctionne pas - fondamentalement, rien n'est mis à jour. Je peux l'utiliser dans l'état où, ou ai-je oublié quelque chose? articles. $. prix: {$ gt: 0} – dferraro