87

I have a document:

{ 'profile_set' :
  [
    { 'name' : 'nick', 'options' : 0 },
    { 'name' : 'joe',  'options' : 2 },
    { 'name' : 'burt', 'options' : 1 }
  ] 
}

and would like to add a new document to the profile_set set if the name doesn't already exist (regardless of the option).

So in this example if I tried to add:

{'name' : 'matt', 'options' : 0}

it should add it, but adding

{'name' : 'nick', 'options' : 2}

should do nothing because a document already exists with name nick even though the option is different.

Mongo seems to match against the whole element and I end up with to check if it's the same and I end up with

profile_set containing [{'name' : 'nick', 'options' : 0}, {'name' : 'nick', 'options' : 2}]

Is there a way to do this with $addToSet or do I have to push another command?

turivishal
  • 34,368
  • 7
  • 36
  • 59
nickponline
  • 25,354
  • 32
  • 99
  • 167

2 Answers2

117

You can qualify your update with a query object that prevents the update if the name is already present in profile_set. In the shell:

db.coll.update(
    {_id: id, 'profile_set.name': {$ne: 'nick'}}, 
    {$push: {profile_set: {'name': 'nick', 'options': 2}}})

So this will only perform the $push for a doc with a matching _id and where there isn't a profile_set element where name is 'nick'.

JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • 1
    If i later need to change mick's name, that is change an existing array object, not add a new one. Is there a way to do that in one atomic update operation that still honor the unique constraint of name? – Anders Östman Oct 27 '14 at 14:23
  • 1
    Follow up question regarding **changing** an existing object in profile_set: http://stackoverflow.com/questions/26590219/change-an-existing-object-in-an-array-but-still-preserve-key-uniqueness. – Anders Östman Oct 27 '14 at 14:49
  • 4
    Everywhere I went to resolve mongoose or mongo problems, @JohnnyHK always saved me. – Jinyoung Kim Mar 30 '16 at 17:04
  • 5
    @JohnnyHK what if I need to overwrite the value if it already exist? – Alex Zhukovskiy Dec 25 '17 at 13:43
  • I think this query is not correct. If there is "nick" and at least one other element, it will return true. for $ne : 'nick'. – Tim T Jun 08 '18 at 03:13
  • @TimT No, it's good. The way `$ne` works with arrays is that it's only a match if _no_ array elements match the value (not just any element). – JohnnyHK Jun 08 '18 at 05:28
  • 3
    Kindly anyone reply to @Alex question above. What is needed to update the element if its already present else push? – raj Jul 26 '18 at 20:02
  • doesn't seem possible to do an upsert in one operation. – Weishi Z Oct 17 '18 at 05:13
  • 1
    Is it possible to use this code when adding multiple objects to an array, so checking all of them for that field? – Miguel Stevens Oct 25 '18 at 06:48
  • This work unless you want to replace using a [key,value] pair in the array, which sounds like the usecase described. – andyczerwonka Nov 12 '18 at 22:45
  • Yeah, this will work. However, if you want to add more values you loose the consistency support between multiple update executions. – Aleydin Karaimin Aug 14 '20 at 07:26
  • Thanks, exactly what I needed! – Maor Barazani Nov 18 '21 at 11:04
13

As of MongoDB 4.2 there is a way to do this using aggregation expressions in update.

For your example case, you would do this:

newSubDocs = [ {'name' : 'matt', 'options' : 0}, {'name' : 'nick', 'options' : 2} ];
db.coll.update( { _id:1 },
[ 
   {$set:  { profile_set:  {$concatArrays: [ 
      "$profile_set",  
      {$filter: {
             input:newSubDocs, 
             cond: {$not: {$in: [ "$$this.name", "$profile_set.name" ]}} 
      }}
   ]}}}
])
Asya Kamsky
  • 41,784
  • 5
  • 109
  • 133
  • MongoError: The dollar ($) prefixed field '$set' in '0.$set' is not valid for storage. – Rafiq Jul 21 '21 at 21:35
  • Sounds like you didn’t get full syntax correct. You might want to ask with all the details in community.mongodb.com – Asya Kamsky Jul 21 '21 at 21:39