Mongodb update deeply nested subdocument

Mongodb

Mongodb Problem Overview


I have a document structure that is deeply nested, like this:

{id: 1, 
 forecasts: [ { 
             forecast_id: 123, 
             name: "Forecast 1", 
             levels: [ 
                { level: "proven", 
                  configs: [
                            { 
                              config: "Custom 1",
                              variables: [{ x: 1, y:2, z:3}]
                            }, 
                            { 
                              config: "Custom 2",
                              variables: [{ x: 10, y:20, z:30}]
                            }, 
                    ]
                }, 
                { level: "likely", 
                  configs: [
                            { 
                              config: "Custom 1",
                              variables: [{ x: 1, y:2, z:3}]
                            }, 
                            { 
                              config: "Custom 2",
                              variables: [{ x: 10, y:20, z:30}]
                            }, 
                    ]
                }
            ]
        }, 
    ]

}

I'm trying to update the collection to insert a new config, that looks like this:

newdata =  {
  config: "Custom 1", 
  variables: [{ x: 111, y:2222, z:3333}]
}

I'm trying something like this in mongo (in Python):

db.myCollection.update({"id": 1, 
                        "forecasts.forecast-id": 123, 
                        "forecasts.levels.level": "proven", 
                        "forecasts.levels.configs.config": "Custom 1"
                         },
                         {"$set": {"forecasts.$.levels.$.configs.$": newData}}
                      )

I'm getting "Cannot apply the positional operator without a corresponding query field containing an array" error though. What is the proper way to do this in mongo? This is mongo v2.4.1.

Mongodb Solutions


Solution 1 - Mongodb

Unfortunately, you can't use the $ operator more than once per key, so you have to use numeric values for the rest. As in:

db.myCollection.update({
    "id": 1, 
    "forecasts.forecast-id": 123, 
    "forecasts.levels.level": "proven", 
    "forecasts.levels.configs.config": "Custom 1"
  },
  {"$set": {"forecasts.$.levels.0.configs.0": newData}}
)

MongoDB's support for updating nested arrays is poor. So you're best off avoiding their use if you need to update the data frequently, and consider using multiple collections instead.

One possibility: make forecasts its own collection, and assuming you have a fixed set of level values, make level an object instead of an array:

{
  _id: 123,
  parentId: 1,
  name: "Forecast 1", 
  levels: {
    proven: { 
      configs: [
        { 
          config: "Custom 1",
          variables: [{ x: 1, y:2, z:3}]
        }, 
        { 
          config: "Custom 2",
          variables: [{ x: 10, y:20, z:30}]
        }, 
      ]
    },
    likely: {
      configs: [
        { 
          config: "Custom 1",
          variables: [{ x: 1, y:2, z:3}]
        }, 
        { 
          config: "Custom 2",
          variables: [{ x: 10, y:20, z:30}]
        }, 
      ]
    }
  }
}

Then you can update it using:

db.myCollection.update({
    _id: 123,
    'levels.proven.configs.config': 'Custom 1'
  },
  { $set: { 'levels.proven.configs.$': newData }}
)

Solution 2 - Mongodb

Managed to solve it with using mongoose:

All you need to know is the '_id's of all of the sub-document in the chain (mongoose automatically create '_id' for each sub-document).

for example -

  SchemaName.findById(_id, function (e, data) {
	  if (e) console.log(e);
	  data.sub1.id(_id1).sub2.id(_id2).field = req.body.something;

      // or if you want to change more then one field -
      //=> var t = data.sub1.id(_id1).sub2.id(_id2);
      //=> t.field = req.body.something;

      data.save();
  });

More about the sub-document _id method in mongoose documentation.

explanation:_id is for the SchemaName, _id1 for sub1 and _id2 for sub2 - you can keep chaining like that.

*You don't have to use findById method, but it's seem to me the most convenient as you need to know the rest of the '_id's anyway.

Solution 3 - Mongodb

MongoDB has introduced ArrayFilters to tackle this issue in Version 3.5.2 and later.

> New in version 3.6. > > Starting in MongoDB 3.6, when updating an array field, you can specify > arrayFilters that determine which array elements to update.

[https://docs.mongodb.com/manual/reference/method/db.collection.update/#specify-arrayfilters-for-an-array-update-operations][1]

Let's say the Schema design as follows :

var ProfileSchema = new Schema({
    name: String,
    albums: [{
        tour_name: String,
        images: [{
            title: String,
            image: String
        }]
    }]
});

And Document created looks like this :

{
   "_id": "1",
   "albums": [{
            "images": [
               {
                  "title": "t1",
                  "url": "url1"
               },
               {
                  "title": "t2",
                  "url": "url2"
               }
            ],
            "tour_name": "london-trip"
         },
         {
            "images": [.........]: 
         }]
}

Say I want to update the "url" of an image. Given - "document id", "tour_name" and "title"

For this the update query :

Profiles.update({_id : req.body.id},
    {
        $set: {

            'albums.$[i].images.$[j].title': req.body.new_name
        }
    },
    {
        arrayFilters: [
            {
                "i.tour_name": req.body.tour_name, "j.image": req.body.new_name   // tour_name -  current tour name,  new_name - new tour name 
            }]
    })
    .then(function (resp) {
        console.log(resp)
        res.json({status: 'success', resp});
    }).catch(function (err) {
    console.log(err);
    res.status(500).json('Failed');
})

Solution 4 - Mongodb

This is a very OLD bug in MongoDB

https://jira.mongodb.org/browse/SERVER-831

Solution 5 - Mongodb

I was facing same kind of problem today, and after lot of exploring on google/stackoverflow/github, I figured arrayFilters are the best solution to this problem. Which would work with mongo 3.6 and above. This link finally saved my day: https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html

const OrganizationInformationSchema = mongoose.Schema({
user: {
    _id: String,
    name: String
},
organizations: [{
    name: {
        type: String,
        unique: true,
        sparse: true
    },
    rosters: [{
        name: {
            type: String
        },
        designation: {
            type: String
        }
    }]
}]
}, {
    timestamps: true
});

And using mongoose in express, updating the name of roster of given id.

const mongoose = require('mongoose');
const ControllerModel = require('../models/organizations.model.js');
module.exports = {
// Find one record from database and update.
findOneRosterAndUpdate: (req, res, next) => {
    ControllerModel.updateOne({}, {
        $set: {
            "organizations.$[].rosters.$[i].name": req.body.name
        }
    }, {
        arrayFilters: [
            { "i._id": mongoose.Types.ObjectId(req.params.id) }
        ]
    }).then(response => {
        res.send(response);
    }).catch(err => {
        res.status(500).send({
            message: "Failed! record cannot be updated.",
            err
        });
    });
}
}

Solution 6 - Mongodb

It's fixed. https://jira.mongodb.org/browse/SERVER-831

But this feature is available starting with the MongoDB 3.5.12 development version.

Note: This question asked on Aug 11 2013 and it's resolved on Aug 11 2017

Solution 7 - Mongodb

Given how MongoDB doesn't appear to provide a good mechanism for this, I find it prudent to use mongoose to simply extract the element from the mongo collection using .findOne(...), run a for-loop search on its relevant subelements (seeking by say ObjectID), modify that JSON, then do Schema.markModified('your.subdocument'); Schema.save(); It's probably not efficient, but it is very simple and works fine.

Solution 8 - Mongodb

I searched about this for about 5 hours and finally found the best and easiest solution: HOW TO UPDATE NESTED SUB-DOCUMENTS IN MONGO DB

{id: 1, 
forecasts: [ { 
         forecast_id: 123, 
         name: "Forecast 1", 
         levels: [ 
            { 
                levelid:1221
                levelname: "proven", 
                configs: [
                        { 
                          config: "Custom 1",
                          variables: [{ x: 1, y:2, z:3}]
                        }, 
                        { 
                          config: "Custom 2",
                          variables: [{ x: 10, y:20, z:30}]
                        }, 
                ]
            }, 
            { 
                levelid:1221
                levelname: "likely", 
                configs: [
                        { 
                          config: "Custom 1",
                          variables: [{ x: 1, y:2, z:3}]
                        }, 
                        { 
                          config: "Custom 2",
                          variables: [{ x: 10, y:20, z:30}]
                        }, 
                ]
            }
        ]
    }, 
]}

Query:

db.weather.updateOne({
                "_id": ObjectId("1"), //this is level O select
                "forecasts": {
                    "$elemMatch": {
                        "forecast_id": ObjectId("123"), //this is level one select
                        "levels.levelid": ObjectId("1221") // this is level to select
                    }
                }
            },
                {
                    "$set": {
                        "forecasts.$[outer].levels.$[inner].levelname": "New proven",
                    }
                },
                {
                    "arrayFilters": [
                        { "outer.forecast_id": ObjectId("123") }, 
                        { "inner.levelid": ObjectId("1221") }
                    ]
                }).then((result) => {
                    resolve(result);
                }, (err) => {
                    reject(err);
                });

Solution 9 - Mongodb

Sharing my lessons learned. I faced the same requirement recently where i need to update a nested array item. My structure is as follows

  {
    "main": {
      "id": "ID_001",
      "name": "Fred flinstone Inc"
    },
    "types": [
      {
        "typeId": "TYPE1",
        "locations": [
          {
            "name": "Sydney",
            "units": [
              {
                "unitId": "PHG_BTG1"
              }
            ]
          },
          {
            "name": "Brisbane",
            "units": [
              {
                "unitId": "PHG_KTN1"
              },
              {
                "unitId": "PHG_KTN2"
              }
            ]
          }
        ]
      }
    ]
  }

My requirement is to add some fields in a specific units[]. My solution is first to find the index of the nested array item (say foundUnitIdx) The two techniques I used are

  1. use the $set keyword

  2. specify the dynamic field in $set using the [] syntax

                query = {
                    "locations.units.unitId": "PHG_KTN2"
                };
                var updateItem = {
                    $set: {
                        ["locations.$.units."+ foundUnitIdx]: unitItem
                    }
                };
                var result = collection.update(
                    query,
                    updateItem,
                    {
                        upsert: true
                    }
                );
    

Hope this helps others. :)

Solution 10 - Mongodb

EASY SOLUTION FOR Mongodb 3.2+ https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/

I had a similar situation and solved it like this. I was using mongoose, but it should still work in vanilla MongoDB. Hope it's useful to someone.

const MyModel = require('./model.js')
const query = {id: 1}

// First get the doc
MyModel.findOne(query, (error, doc) => {

    // Do some mutations
    doc.foo.bar.etc = 'some new value'
    
    // Pass in the mutated doc and replace
    MyModel.replaceOne(query, doc, (error, newDoc) => {
         console.log('It worked!')
    })
}

Depending on your use case, you might be able to skip the initial findOne()

Solution 11 - Mongodb

Okkk.we can update our nested subdocument in mongodb.this is our schema.

var Post = new mongoose.Schema({
    name:String,
    post:[{
        like:String,
        comment:[{
            date:String,
            username:String,
            detail:{
                time:String,
                day:String
            }
        }]
    }]
})

solution for this schema

  Test.update({"post._id":"58206a6aa7b5b99e32b7eb58"},
    {$set:{"post.$.comment.0.detail.time":"aajtk"}},
          function(err,data){
//data is updated
})

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionreptilicusView Question on Stackoverflow
Solution 1 - MongodbJohnnyHKView Answer on Stackoverflow
Solution 2 - MongodbNoam ElView Answer on Stackoverflow
Solution 3 - MongodbNIKHIL C MView Answer on Stackoverflow
Solution 4 - Mongodbvictor sosaView Answer on Stackoverflow
Solution 5 - MongodbTHE INN-VISIBLEView Answer on Stackoverflow
Solution 6 - Mongodb6339View Answer on Stackoverflow
Solution 7 - MongodbEngineerView Answer on Stackoverflow
Solution 8 - Mongodbali homayouniView Answer on Stackoverflow
Solution 9 - Mongodberickyi2006View Answer on Stackoverflow
Solution 10 - MongodbAaronCodingView Answer on Stackoverflow
Solution 11 - MongodbcoderView Answer on Stackoverflow