sambomartin
sambomartin

Reputation: 6813

Update nested array document

say i have this model

{
    _id : 1,
    ref: '1',
    children: [
    {
        ref:'1.1',
        grandchildren: [
            {
                ref:'1.1.1',
                visible: true;
            }
        ]
    }
    ]
}

I'm aware that positional operator for nested arrays isn't available yet.

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

but wondered whether its possible to atomically update the document in the nested array?

In my example, i'd like to update the visible flag to false for the document for ref 1.1.1.

I have the children record ref == '1.1' and the grandchildrenref == '1.1.1'

thanks

Upvotes: 4

Views: 1500

Answers (1)

chridam
chridam

Reputation: 103305

Yes, this is possible only if you knew the index of the children array that has the grandchildren object to be updated beforehand and the update query will use the positional operator as follows:

db.collection.update(
    {
        "children.ref": "1.1",
        "children.grandchildren.ref": "1.1.1"
    }, 
    {
        "$set": { 
            "children.0.grandchildren.$.visible": false
        }
    }
)

However, if you don't know the array index positions beforehand, you should consider creating the $set conditions dynamically by using MapReduce. The basic idea with MapReduce is that it uses JavaScript as its query language but this tends to be fairly slower than the aggregation framework and not recommended for use in real-time data analysis.

In your MapReduce operation, you need to define a couple of steps i.e. the mapping step (which maps an operation into every document in the collection, and the operation can either do nothing or emit some object with keys and projected values) and reducing step (which takes the list of emitted values and reduces it to a single element).

For the map step, you ideally would want to get for every document in the collection, the index for each children array field and another key that contains the $set keys.

Your reduce step would be a function (which does nothing) simply defined as var reduce = function() {};

The final step in your MapReduce operation will then create a separate collection operations that contains the emitted operations array object along with a field with the $set conditions. This collection can be updated periodically when you run the MapReduce operation on the original collection. Altogether, this MapReduce method would look like:

var map = function(){
    for(var i = 0; i < this.children.length; i++){
        emit( 
            {
                "_id": this._id, 
                "index": i 
            }, 
            {
                "index": i, 
                "children": this.children[i],            
                "update": {
                    "ref": "children." + i.toString() + ".grandchildren.$.ref",
                    "visible": "children." + i.toString() + ".grandchildren.$.visible"
                }                    
            }
        );
    }
};

var reduce = function(){};

db.collection.mapReduce(
    map,
    reduce,
    {
        "out": {
            "replace": "update_collection"
        }
    }
);

You can then use the cursor from the db.update_collection.find() method to iterate over and update your collection accordingly:

var cur = db.update_collection.find(
    {
        "value.children.ref": "1.1",
        "value.children.grandchildren.ref": "1.1.1" 
    }
);

// Iterate through results and update using the update query object set dynamically by using the array-index syntax.
while (cur.hasNext()) {
    var doc = cur.next();
    var update = { "$set": {} };
    // set the update query object
    update["$set"][doc.value.update.visible] = false;

    db.collection.update(
        {
            "children.ref": "1.1",
            "children.grandchildren.ref": "1.1.1"
        }, 
        update 
    );
};

Upvotes: 3

Related Questions