33

I have a collection which elements can be simplified to this:

{tags : [1, 5, 8]}

where there would be at least one element in array and all of them should be different. I want to substitute one tag for another and I thought that there would not be a problem. So I came up with the following query:

db.colll.update({
  tags : 1
},{
  $pull: { tags: 1 },
  $addToSet: { tags: 2 }
}, {
  multi: true
})

Cool, so it will find all elements which has a tag that I do not need (1), remove it and add another (2) if it is not there already. The problem is that I get an error:

"Cannot update 'tags' and 'tags' at the same time"

Which basically means that I can not do pull and addtoset at the same time. Is there any other way I can do this?

Of course I can memorize all the IDs of the elements and then remove tag and add in separate queries, but this does not sound nice.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Salvador Dali
  • 214,103
  • 147
  • 703
  • 753

4 Answers4

25

The error is pretty much what it means as you cannot act on two things of the same "path" in the same update operation. The two operators you are using do not process sequentially as you might think they do.

You can do this with as "sequential" as you can possibly get with the "bulk" operations API or other form of "bulk" update though. Within reason of course, and also in reverse:

var bulk = db.coll.initializeOrderedBulkOp();
bulk.find({ "tags": 1 }).updateOne({ "$addToSet": { "tags":  2 } });
bulk.find({ "tags": 1 }).updateOne({ "$pull": { "tags": 1 } });

bulk.execute();

Not a guarantee that nothing else will try to modify,but it is as close as you will currently get.

Also see the raw "update" command with multiple documents.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • Thanks for telling about bulk update, but for some reason I am `getting: "errmsg" : "Modifiers operate on fields but we found a NumberDouble instead. For example: {$mod: {: ...}} not {$addToSet: 183.0}",` But I am sending parseInt(ID) – Salvador Dali Jun 19 '14 at 07:30
  • @SalvadorDali Edited too quickly. The field names were missing. – Neil Lunn Jun 19 '14 at 07:50
  • @NeilLunn Is this going to be added as a feature or what? I have the same problem - pushing and pulling at the same time. It seems normal to want to do it and I would expect to be able to do such a thing. Should I issue a feature request to mongo jira? – Ev0oD Aug 27 '14 at 11:46
  • 1
    Please note that using two operations in a bulk operation is *not* atomic. It is possible that the first operation succeeds, but the second one doesn't; or that there is a race condition. – Derick Feb 07 '17 at 17:14
7

Starting in Mongo 4.4, the $function aggregation operator allows applying a custom javascript function to implement behaviour not supported by the MongoDB Query Language.

And coupled with improvements made to db.collection.update() in Mongo 4.2 that can accept an aggregation pipeline, allowing the update of a field based on its own value,

We can manipulate and update an array in ways the language doesn't easily permit:

// { "tags" : [ 1, 5, 8 ] }
db.collection.updateMany(
  { tags: 1 },
  [{ $set:
    { "tags":
      { $function: {
          body: function(tags) { tags.push(2); return tags.filter(x => x != 1); },
          args: ["$tags"],
          lang: "js"
      }}
    }
  }]
)
// { "tags" : [ 5, 8, 2 ] }

$function takes 3 parameters:

  • body, which is the function to apply, whose parameter is the array to modify. The function here simply consists in pushing 2 to the array and filtering out 1.
  • args, which contains the fields from the record that the body function takes as parameter. In our case, "$tag".
  • lang, which is the language in which the body function is written. Only js is currently available.
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
5

If you're removing and adding at the same time, you may be modeling a 'map', instead of a 'set'. If so, an object may be less work than an array.

Instead of data as an array:

{ _id: 'myobjectwithdata',
  data: [{ id: 'data1', important: 'stuff'},
         { id: 'data2', important: 'more'}]
}

Use data as an object:

{ _id: 'myobjectwithdata',
  data: { data1: { important: 'stuff'},
          data2: { important: 'more'} }
}

The one-command update is then:

db.coll.update(
  'myobjectwithdata', 
  { $set: { 'data.data1': { important: 'treasure' } }
);

Hard brain working for this answer done here and here.

Shaunak Shukla
  • 2,347
  • 2
  • 18
  • 29
Michael Cole
  • 15,473
  • 7
  • 79
  • 96
2

In case you need replace one value in an array to another check this answer:

Replace array value using arrayFilters

Nikolay Prokopyev
  • 1,260
  • 12
  • 22