I just upgraded to Mongo 2.6.1 and one update statement that was working before is not returning an error. The update statement is:

db.post.update( { 'answers.comments.name': 'jeff' },
    { '$set': {
        'answers.$.comments.$.name': 'joe'
    }},
    { multi: true }
)

The error I get is:

WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 2,
        "errmsg" : "Too many positional (i.e. '$') elements found in path 'answers.$.comments.$.createUsername'"
    }
})

When I update an element just one level deep instead of two (i.e. answers.$.name instead of answers.$.comments.$.name), it works fine. If I downgrade my mongo instance below 2.6, it also works fine.

Solution 1

You CAN do this, you just need Mongo 3.6! Instead of redesigning your database, you could use the Array Filters feature in Mongo 3.6, which can be found here:

https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters

The beauty of this is that you can bind all matches in an array to a variable, and then reference that variable later. Here is the prime example from the link above:

Solution 2

Use arrayFilters.

MongoDB 3.5.12 extends all update modifiers to apply to all array elements or all array elements that match a predicate, specified in a new update option arrayFilters. This syntax also supports nested array elements.

Let us assume a scenario-

"access": {
    "projects": [{
        "projectId": ObjectId(...),
        "milestones": [{
            "milestoneId": ObjectId(...),
            "pulses": [{
                "pulseId": ObjectId(...)
            }]
        }]
    }]
}

Now if you want to add a pulse to a milestone which exists inside a project

db.users.updateOne({
    "_id": ObjectId(userId)
}, {
    "$push": {
        "access.projects.$[i].milestones.$[j].pulses": ObjectId(pulseId)
    }
}, {
    arrayFilters: [{
        "i.projectId": ObjectId(projectId)
    }, {
        "j.milestoneId": ObjectId(milestoneId)
    }]
})

For PyMongo, use arrayFilters like this-

db.users.update_one({
    "_id": ObjectId(userId)
}, {
    "$push": {
        "access.projects.$[i].milestones.$[j].pulses": ObjectId(pulseId)
    }
}, array_filters = [{
        "i.projectId": ObjectId(projectId)
}, {
        "j.milestoneId": ObjectId(milestoneId)
}])

Also,

Each array filter must be a predicate over a document with a single field name. Each array filter must be used in the update expression, and each array filter identifier $[] must have a corresponding array filter. must begin with a lowercase letter and not contain any special characters. There must not be two array filters with the same field name.

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

Solution 3

The positional operator can be used only once in a query. This is a limitation, there is an open ticket for improvement: https://jira.mongodb.org/browse/SERVER-831

Solution 4

As mentioned; more than one positional elements not supported for now. You may update with mongodb cursor.forEach() method.

db.post
  .find({"answers.comments.name": "jeff"})
  .forEach(function(post) {
    if (post.answers) {
      post.answers.forEach(function(answer) {
        if (answer.comments) {
          answer.comments.forEach(function(comment) {
            if (comment.name === "jeff") {
              comment.name = "joe";
            }
          });
        }
      });

      db.post.save(post);
    }
  });

Solution 5

I have faced the same issue for the as array inside Array update require much performance impact. So, mongo db doest not support it. Redesign your database as shown in the given link below.

https://pythonolyk.wordpress.com/2016/01/17/mongodb-update-nested-array-using-positional-operator/

Solution 6

    db.post.update(
    { 'answers.comments.name': 'jeff' },
    { '$set': {
        'answers.$[i].comments.$.name': 'joe'
    }},
    {arrayFilters: [ { "i.comments.name": { $eq: 'jeff' } } ]}
)
check path after answers for get key path right

Solution 7

db.post.update( { 'answers.comments.name': 'jeff' },
    { '$set': {
        'answers.$.comments.$.name': 'joe'
    }},
    { multi: true }
)

Answer is

db.post.update( { 'answers.comments.name': 'jeff' },
    { '$set': {
        'answers.0.comments.1.name': 'joe'
    }},
    { multi: true }
)