Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding values at front with $addToSet modifier

I am trying to run an update query on mongo, which will add few values in an existing array property.

My goal is to add the elements at the beginning of the array, instead of at the end, which is happening by default. Here is the code I am using:

db.collection("Project").update({'_id': group.project}, {$addToSet: {'endpointOrder': {$each: group.endpointOrder, $position: 0}}}, {multi: true})

As you can see above, if I just change the 'addToSet' to use 'push' modifier, then works fine, but that will keep the duplicates, which can't be. So, my assumption is that, $position is only supported for '$push' , not for '$addToSet'.

Any idea/suggestions about how can I achieve that? Thanks.

like image 573
Rana Avatar asked Jul 07 '15 18:07

Rana


1 Answers

The $addToSet operator produces a "set" of "unique" items which is likely what you are after. Unfortunately there is a common mantra through MongoDB for this and other "set operators":

"Sets are not considered to be ordered"

So when you add items into a "set" ( or combine with other operations ) there is no guarantee that the items appears in any position whether it be the beginning or the end of the "set". The official line is that any element could appear in any order, and it does not matter as long as it is part of the "set".

But there is another way around this, and it simply means that you do not use $addToSet but instead use some other filtering logic in combination with $push:

Consider the document:

{ "a": [ 3,1,2 ] }

So if I wanted to add a new element 4 to the list and prepend it at the "first" position where the element is not already part of the "set", then you just construct the query and update like this:

db.collection.update(
    { "a": { "$ne": 4 } },
    { "$push": { "a": { "$each": [4], "$position": 0 } } }
)

In brief: "Don't push to the array if the value is already present in the array."

And really that is all there is too it. If you asked to add an element that was already present then of course the query condition would be false and nothing would be modified.

In the end you get the same results as you would with an $addToSet operation, except you have complete control over element positioning or even "sorting" as required. All you need to do is check for the matching element before deciding whether to change anything.


A procedure for handling multiple elements should really be using the "Bulk" operations API for the most efficiency

var items = [4,3];

var bulk = db.collection.initializeOrderedBulkOp();
bulk.find({ "a": { "$nin": items } }).updateOne(
    { "$push": { "a": { "$each": items, "$position": 0 } } }
);

items.forEach(function(item) {
    bulk.find({ "a": { "$ne": item } }).updateOne(
        { "$push": { "a": { "$each": [item], "$position": 0 } } }
    );
});
bulk.execute();    

Which would result in only one of the update operations within the loop actually matching or modifying anything:

{ "a": [ 4,3,1,2 ] }

Or with values that do not exist at all:

var items = [5,8];

var bulk = db.collection.initializeOrderedBulkOp();
bulk.find({ "a": { "$nin": items } }).updateOne(
    { "$push": { "a": { "$each": items, "$position": 0 } } }
);

items.forEach(function(item) {
    bulk.find({ "a": { "$ne": item } }).updateOne(
        { "$push": { "a": { "$each": [item], "$position": 0 } } }
    );
});
bulk.execute();    

Then the first operation would actually modify and the looped instructions will not match anything since the items are already there.

{ "a": [ 5,8,4,3,1,2 ] }
like image 144
Blakes Seven Avatar answered Sep 28 '22 11:09

Blakes Seven