ChatGPT解决这个技术问题 Extra ChatGPT

How do you update objects in a document's array (nested updating)

Assume we have the following collection, which I have few questions about:

{
    "_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
            }
    ]
}

I want to increase the price for "item_name":"my_item_two" and if it doesn't exists, it should be appended to the "items" array. How can I update two fields at the same time? For example, increase the price for "my_item_three" and at the same time increase the "total" (with the same value).

I prefer to do this on the MongoDB side, otherwise I have to load the document in client-side (Python) and construct the updated document and replace it with the existing one in MongoDB.

This is what I have tried and works fine if the object exists:

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

However, if the key doesn't exist, it does nothing. Also, it only updates the nested object. There is no way with this command to update the "total" field as well.

I think you can't do this in mongo, except maybe with much pain using the eval. Mongo is very limited in data ops.
@Haapala: mongodb has $inc and update with upsert
@jdi yes yes, but it does not help here much, but what he needs is multiple $incs, conditionally, and if the item does not exist, then a $push is needed.

k
k107

For question #1, let's break it into two parts. First, increment any document that has "items.item_name" equal to "my_item_two". For this you'll have to use the positional "$" operator. Something like:

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

Note that this will only increment the first matched subdocument in any array (so if you have another document in the array with "item_name" equal to "my_item_two", it won't get incremented). But this might be what you want.

The second part is trickier. We can push a new item to an array without a "my_item_two" as follows:

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

For your question #2, the answer is easier. To increment the total and the price of item_three in any document that contains "my_item_three," you can use the $inc operator on multiple fields at the same time. Something like:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);

Thanks for the reply. The answer for Question#2 is perfect and works fine :) But for Question#1, the problem is you should search for the document by "user_id", and not by "items.item_name". In this case I can not use the Positional Operator, as I haven't specified the array in the search query. Also I want both parts to be done in one shot. (if possible)
Nice examples. I felt like I was making this direction apparent from the links in my answer, but I kept getting resistance saying its not what he was looking for. Guess the OP just needed to see examples on how to do multiple $inc.
For question #1, you should still be able to do it in two parts by adding the user_id to the query bit of each of the updates (I'll modify the answer accordingly). Will have to think more about whether it's possible to do in a single shot.
What are the 3rd and 4th parameters in the update method? and why?
@skmahawar regarding the 3rd and 4th params, docs.mongodb.com/manual/reference/method/db.collection.update indicates these options are for "upsert" and "multi" respectively. For upsert, if set to true, creates a new document when no document matches the query criteria. The default value is false, which does not insert a new document when no match is found." For multi, If set to true, updates multiple documents that meet the query criteria. If set to false, updates one document. The default value is false.
g
gustavohenke

There is no way to do this in single query. You have to search the document in first query:

If document exists:

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);

No need to add condition {$ne : "my_item_two" }.

Also in multithreaded enviourment you have to be careful that only one thread can execute the second (insert case, if document did not found) at a time, otherwise duplicate embed documents will be inserted.


no, there is a way to do this in a single query, see above
@MartijnScheffer, I don't see a way to do this as a single query anywhere in this thread. Even the "answer" is using 2 queries the same as in this answer. Am I missing something? I am also hoping there is a way to do this in one query to prevent any multi threading issues
@Udit, how can I use the items.$.price inside the condition? when I try to add a condition such as "increment only if the price is greater than 0", it doesn't work - basically nothing gets updated. I can I use this in the where condition, or am I missing something? items.$.price : { $gt : 0 }
something must have dissapeared :)
K
KARTHIKEYAN.A

We can use $set operator to update the nested array inside object filed update the value

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

m
mockingmoon

One way to ensure there are no duplicates of the "item_name" fields would be the do the same actions as given in the answer https://stackoverflow.com/a/10523963 for Question #1 but in reverse order!

Push into the document if "items.item_name":{"$ne":"my_name"} - the update filter MUST contain some uniquely indexed field! And upsert is false. Increment the document if "items.item_name":"my_name".

The first update should be atomic, thus it doesn't do anything if the array already contains the element with the item_name "my_name".

By the time the second update happens, there must be an array element with the "item_name"="my_name"