Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I $addToSet an object to an array and $sort too using MongoDB?

Tags:

arrays

mongodb

I need to add an object to an array in a MongoDB collection document, after inserting it I need to make sure all array's elements get ordered by one of their properties.

As I need objects in array to be unique, I use $addToSet instead of $push. This is an example of what I'm trying:

db.perros.update(
    {name: "Risas"}, 
    {
        $addToSet: {propiedades: {name: "cola", cantidad: 1}}, 
        $push: { propiedades: { $each: [ ], $sort: {cantidad: -1} }}
    });

However this will fail with the following Mongo error:

WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 16837,
        "errmsg" : "Cannot update 'propiedades' and 'propiedades' at the same time"
    }
})

And this is obvious when you think about sets, however $push operation doesn't even care what's inside of array ... How could achieve this without having to use $push?

like image 264
diegoaguilar Avatar asked Jul 10 '15 23:07

diegoaguilar


1 Answers

You were part of the way there by correctly identifying the operations you need to do. But of course $sort is not a valid modifier for $addToSet since the MongoDB mantra is "sets are not considered to be ordered":

$addToSet does not guarantee a particular ordering of elements in the modified set.

The other problem here as indicated by the error is that you cannot use multiple update operators ( such as $addToSet and $push ) on the same path to a property at the same time. There is in fact "no order" to the execution of different update operators, so their is no guarantee that the $addToSet occurs before the $push. In fact they are likely acting in parallel, which is why the error and that this is not allowed.

The answer of course is "two" update statements. One for the $addToSet and one to apply the $sort by "pushing" an empty array via $each,

But since we really don't want to "wait" for each update to complete, this is what the "Bulk" operations API is for. So you can sent both instructions to the server in one send and get one response:

var bulk = db.perros.initializeOrderedBulkOp();
bulk.find({ "name": "Risas" }).update({ 
   "$addToSet": { 
       "propiedades": { "name": "cola", "cantidad": 1 }
   }
});
bulk.find({ "name": "Risas" }).update({ 
   "$push": { 
       "propiedades": { 
           "$each": [ ], "$sort": { "cantidad": -1 } 
        }
   }
});
bulk.execute();

So this really is still only one request to the server and one response. It is still "two" operations but the overhead and possibility of some thread grabbing the interim state of the upadte is negligable.

There is an alternative to this approach which is to move the "set detection" logic into the .find() portion of the update statement and then just apply $push where the member(s) to be added to the "set" do not already exist:

var bulk = db.perros.initializeOrderedBulkOp();
bulk.find({ 
    "name": "Risas", 
    "propiedades": { 
        "$not": { "$elemMatch": { "name": "cola", "cantidad": 1 } } 
    } 
}).update({ 
   "$push": { 
       "propiedades": { 
           "$each": [{ "name": "cola", "cantidad": 1 }], "$sort": { "cantidad": -1 } 
        }
   }
});
bulk.execute();

Of course the complication there is that if you are adding "multiple" array elements here you would need to wrap those $not and $elemMacth tests in an $and condition, and then if "only one" of those elements was valid then it could not be added alone.

You can "try" that sort of operation with "multiple" items "first", but then you should have a "fallback" execution of each individual array element with the same logic as above to "test" the possibilty of "pushing" for each one.

So $addToSet makes that second part easy with multiple array entries. For one entry it's quite simple to just "query" and $push, for more than one it is probably the shorter path to use the "first" pattern with $addToSet and $push an empty array to "sort" the result since applying the second pattern means multiple update tests anyway.

like image 149
Blakes Seven Avatar answered Oct 10 '22 00:10

Blakes Seven