I am doing a findOneAndUpdate call to add to a subdocument array using $addToSet. The operation returns the newly updated document. I would like to extract the subdocument that just got inserted. Is it safe to use pop() in this case? I know that browsers vary in how arrays/objects are ordered but not sure about the node/mongo case.
The general official line for "sets" in MongoDB is that they are not considered to be ordered in any way. There actually has been some discussion that "sets" may even constitute a different data type to that of an ordinary array at some point, but as yet there is no distinction made between types and they are internally treated just as another array.
It would appear however, that the general behavior actually is that any "new" item added to the set is indeed "appended" to the list of current elements, so anything new would indeed be the last element in an array. And arrays always do retain positions, which is the general point of the data type.
So whilst your point is basically asking:
"will the last added item be the last in the list?"
Then that would appear to be the case despite the given cases warning about the order of elements, which would generally appear to be from surface value more about the "internal" ordering, where adding "a" after "c" would not place the "a" element first in the list without additional modification.
But the question also comes off a little bit "funny" in that you are essentially asking:
"did my last update to the set appear on the end?"
Which IMHO seems to be really asking "Did it update or not?", or did the last member I added actually update the "set" or did the element already exist. In this case, rather than looking to see if the last element is the same as the one you wanted to add, the better option would be to check the WriteResult and see if the document was actually modified.
Consider this document:
{
"list": [ "c", "a" ]
}
And then issuing the the update with the Bulk Operations API to get the result:
var bulk = Model.collection("settest").initializeOrderedBulkOp();
bulk.find({}).updateOne({ "$addToSet": { "list": "a" } });
bulk.execute(function(err,result) {
console.log( JSON.stringify( result, undefined, 4 ) );
});
Would give you something like this:
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 0,
"nUpserted" : 0,
"nMatched" : 1,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
The enhanced response tells you that even though 1 document was matched by the update statement, nothing was actually modified in the document itself since the $addToSet operation actually did not do anything where the element was already present in the set.
If you however considered the mongoose .findOneAndUpdate() method, which is derived from the basic underlying .findAndModify() method of the base driver, then the response you get back is the "modified" document by default:
Model.findOneAndUpdate(
{},
{ "$addToSet": { "list": "a" },
function(err,doc) {
console.log( JSON.stringify( doc, undefined, 4 ) );
}
);
So that is basically the same document as before, and there is no way to tell if the update operation actually did anything or not.
Now you could modify that to ask for the original document instead of the modified one:
Model.findOneAndUpdate(
{},
{ "$addToSet": { "list": "a" },
{ "new": false },
function(err,doc) {
console.log( JSON.stringify( doc, undefined, 4 ) );
}
);
But the only thing you really know about the result is that if the element you last added is not present in the original document, then that resulted in an update. Of course it is there "now" because you asked it to do that and the operation succeeded and returned you a document:
Model.findOneAndUpdate(
{},
{ "$addToSet": { "list": "a" },
{ "new": false },
function(err,doc) {
if ( doc.list.indexOf("a") == -1 )
console.log( "updated set" );
}
);
But is the "last element" in the list where added? As covered earlier, it would appear so, but whether this always remains the same is the question. But it really does not make a lot of sense to do so as you cannot guarantee that the operation you just did actually did anything without checking for it properly.
So in an atomic operation such as .findOneAndUpdate() where you get the document returned, or indeed in any "update" variant, the better option to "did this do anything" is to actually check the Write Result or the original document to what you expect the result of the operation to be.
In the end, if you need the whole document back then use .findOneAndUpdate() but with the "original document" option. You can safely assume that your operation succeeded and add the element to the list where it was not present originally if that is your desired result. The difference tells you if the update actually did anything.
If however you only want to know if a new element was indeed added, then just use the Bulk API approach, which gives you the correct response that the document was not updated since the element was already part of the "set".