100

I'm using MongoDB to be my database. i have a data:

 {
   _id : '123'
   friends: [
     {name: 'allen', emails: [{email: '11111', using: 'true'}]}
   ]
 }

now, i wanna to modify user's friends' emails ' email, whose _id is '123' i write like this:

db.users.update ({_id: '123'}, {$set: {"friends.0.emails.$.email" : '2222'} })

it's easy, but , it's wrong , when the emails array has two or more data. so, my question is: how can i modify the data in a nested filed --- just have two or more nested array? Thanks.

LewoSoft
  • 73
  • 1
  • 2
  • 12
allen wang
  • 1,087
  • 2
  • 7
  • 16
  • 1
    It's not clear what problem you have. "*it's easy, but , it's wrong , when the emails array has two or more data*" - describe the expected and the actual behavior of the system. – Cristian Lupascu Oct 26 '13 at 09:40
  • for example , 'emails' has two objects, like below: emails [ {email: '1111', using: 'true'}, {email: '2222', using: 'ture'}] . how can i set the second object using $set ? the method i used is : {$set: {"friends.0.emails.$.email" : '2222'}, but, it just find the emails's first object,it can't help me find the specific one. – allen wang Oct 26 '13 at 12:43

5 Answers5

125

You need to use the Dot Notation for the arrays.

That is, you should replace the $ with the zero-based index of the element you're trying to update.

For example:

db.users.update ({_id: '123'}, { '$set': {"friends.0.emails.0.email" : '2222'} });

will update the first email of the first friend, and

db.users.update ({_id: '123'}, { '$set': {"friends.0.emails.1.email" : '2222'} })

will update the second email of the first friend.

Cristian Lupascu
  • 39,078
  • 16
  • 100
  • 137
  • that's right. i got it. think further, how could i modify the very email but i don't konw the position of it in emails? thank you for your answer! – allen wang Oct 28 '13 at 02:00
  • @allenwang I don't think such complex logic is supported in the `update` statement. You should probably retrieve the record by `_id` using `findOne()`, do any manipulations you want with JavaScript and then [`save()`](http://docs.mongodb.org/manual/reference/method/db.collection.save/) it back. – Cristian Lupascu Oct 28 '13 at 06:41
  • @wOlf thanks! i think so , but i hope mongo will support double '$' to solve this question. – allen wang Oct 28 '13 at 08:45
  • If you are doing this in Python you might want to quote the '$set' like so. – aaa90210 Nov 19 '15 at 05:13
  • @aaa90210 You're right; thanks for your suggestion! I have edited my answer. – Cristian Lupascu Nov 19 '15 at 09:19
  • you will lose atomicity if you pull and then save later. i.e. a race condition could overwrite data if you have multiple threads or servers – Matt May 02 '17 at 21:48
  • @GolfWolf Could you see my problem https://stackoverflow.com/questions/48111526/how-to-update-single-object-with-restheart-patch/48135366?noredirect=1#comment83251598_48135366 – Varun Sharma Jan 07 '18 at 17:10
  • if anybody needs to create key string in js objects see this issue https://stackoverflow.com/questions/11508463/javascript-set-object-key-by-variable/11508490#11508490 tnx @CristianLupascu – khodekazemi Aug 12 '20 at 15:43
  • I have a doubt what if I don't know the index of my key in mongodb array, then how can I update my collection. Let's say I want to update `using` as `False` when the `_id` is `'123'` and `'name'` is `'george'`, but I don't know the mongodb array index. Is there a way to do that – Yashasvi Bhatt Apr 13 '21 at 07:14
24

One flexible way to do updates to a multilevel array is to use arrayFilters which allows indexes to be queried for and assigned to an identifier.

db.collection.update(
   { <query selector> },
   { <update operator>: { "array.$[<identifier>].field" : value } },
   { arrayFilters: [ { <identifier>: <condition> } } ] }
)

Here's the example you provided plus a new friend with two emails:

{
   _id : '123'
   friends: [
     {name: 'allen', emails: [
        {email: '11111', using: 'true'}
     ]},
     {name: 'lucy' , emails: [
        {email: 'lucy@work.com', using:'true'}, 
        {email:'lucyGucy@zmail.com', using : 'false'}
     ]}
   ]
 }

Suppose lucyGucy@zmail.com is being updated to lucy.is.gucy@zmail.com. We can use the name and email fields in an array filter to identify the index we want to update.

db.users.update({_id:123}, {$set:{
    "friends.$[updateFriend].emails.$[updateEmail].email" : "lucy.is.gucy@zmail.com"
}}, {
    "arrayFilters": [
      {"updateFriend.name" : "lucy"},
      {"updateEmail.email" : "lucyGucy@zmail.com"}
    ]
})

In this way the update is not sensitive to a particular array order. The example above uses two identifiers updateFriend and updateEmail which will match – for the array they are applied too – any elements fulfilling the array filter criteria. As noted in the documentation:

The <identifier> must begin with a lowercase letter and contain only alphanumeric characters.

Also while emails are likely unique I would recommend including a unique id on all subdocuments so the arrayFilters can be exact.

Kanembel
  • 434
  • 4
  • 11
3

Solution using Mongoose:

    Users.findById("123", function(err, user) {

      var friends = user.friends;
        for ( i=0; i < friends.length; i++ ) {
          if (friends[i].name == 'allen') {
            friends[i].email = '2222';

            user.save(function(err) {
              if (err) throw err;
              console.log("email updated");
            });
          } else {
            console.log("no friends named allen");
          }
        }

    }
Kacper
  • 47
  • 3
0

update something in a multi level arry is really pain in the ass, my way of doing it:replace the deep level arry.

db.user.findOne({_id:'123'},{friends:1}).lean().exec(function(err,user){
   var whichArrayToUpdate;
   for (var ii = 0; ii < user.friends.length; ii++) {
        for (var jj = 0; i < user.friends[ii].emails; jj++) {
            if(user.friends[ii].emails[jj].email == '1111' ){// update it below

                user.friends[ii].emails[jj].email == 'what ever you want to set to.';

                whichArrayToReplace = user.friends[ii].emails;
                break;
            }
        };
   };

   db.user.update({'friends.name':'allen'},{$set{'friends.$.email': whichArrayToReplace} })
})

but, why not use the save() method? the save() will replace your whole document, if your document is small that's ok, but if your document is relly big , it's a better idea to replace just part of your document.

or do the loop, use the position of the top level array and second level array(ii and jj) to update.

my advice is: when you design schema, don't put array in another array unless you won't do any update for that array.

Eisneim
  • 97
  • 1
  • 5
-1

I have a similar situation where I have main category such as:

{
    "service": {
        "isReviewed": false,
        "_id": "5ea36b27d7ae560845afb88d",
        "mainCategory": "message ",
        "SubCategory": [
            {
                "priceChanged": true,
                "_id": "5ea36b27d7ae560845afb88e",
                "subCatPrice": "26",
                "subCatName": "mustach trimming"
            }
        ],
        "shopName": "paddy the barber",
        "user": "5ea1fd5b69512dc72ad2f48c",
        "date_created": "2020-04-24T22:41:43.368Z",
        "__v": 5
    }
}

Now, it took a while figure out how to update one piece of the subdocument. as the sub doc is store as an object.

So All id, is first I check the _id of the doc when its passed through the put/:id. Then, I checked the owner of doc against the req.user.

if everything okay, I looped through the array then I produced a new object. Then, all I did, is I check each req.body for price or name, if there is a new price, I would just update the price value in the object. if there is new value then the object remains the same.

then all I said, service.SubCategory = newObject and store the data.

Its working fine, however, I ran into the problem of subdoc _id getting updated, so I fixed that by keeping the old value as its.

Now, in terms of performance and optimization, I am not entirely sure if this is the correct way but its working and if they're better away I am willing to change.

Here is the code:

const newObject = {
      subCatPrice: "",
      subCatName: "",
      _id: "",
      priceChanged: false
    };

    let oldDetails = service.SubCategory;
    for (const old of oldDetails) {
      newObject.subCatPrice = old.subCatPrice;
      newObject.subCatName = old.subCatName;
      newObject._id = old._id;
    }

    price ? (newObject.subCatPrice = price ) && (newObject.priceChanged = true)  : newObject.subCatPrice;
    name ? (newObject.subCatName = name) : newObject.subCatName;

    (service.SubCategory = newObject), await service.save();

I used the react idea of the state to be honest, where I just get a copy of the object and keep it and apply an update to the piece that I want to update.

Again in terms of clean code and all of that stuff, I am not sure if this is the correct way to do it, but I am also a student of programming and I would like to learn too.

Hope this help someone

adam
  • 49
  • 2
  • 6