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
?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With