How do I update MongoDB document fields only if they don't exist?

I have collection foo with documents like:

{site_id: 'xxx', title: {ru: 'a', en: 'b'}, content: {ru: 'a', en: 'b'}}
{site_id: 'xxx', title: {ru: 'c', de: 'd'}, content: {ru: 'c', de: 'd'}}

I need to update multiple fields which are can exists or not:

db.foo.update(
    { site_id: 'xxx'},
    { $set: {'title.de': '', 'content.de': ''}},
    {multi: true}
)

But I need something like $set which will not overwrite value if it exists.


Solution 1:

You can add a query to your update statement:

db.foo.update({'title.de': {$exists : false}}, {$set: {'title.de': ''}})

Update

For your modified question my solution looks like this - would that work for you? (If not, why?)

db.foo.update({site_id: 'xxx', 'title.de': {$exists : false}}, {$set: {'title.de': ''}, {multi: true})
db.foo.update({site_id: 'xxx', 'content.de': {$exists : false}}, {$set: {'content.de': ''}}, {multi: true})

Solution 2:

Starting Mongo 4.2, db.collection.update() can accept an aggregation pipeline, finally allowing the update/creation of a field based on another field:

This way, we can move field checks within the update stage rather than within the match stage, thus making it a one-pass update:

// { site_id: "xxx", title: { ru: "a", en: "b" }, content: {} }
// { site_id: "xxx", title: { ru: "c", de: "d" }, content: { ru: "c" } }
db.collection.update(
  { site_id: "xxx" },
  [{ $set: {
    "title.de": { $cond: [ { $not: ["$title.de"] }, "", "$title.de" ] },
    "content.ru": { $cond: [ { $not: ["$content.ru"] }, "", "$content.ru" ] }
  }}],
  { multi: true }
)
// { site_id: "xxx", title: { ru: "a", en: "b", de: "" }, content: { ru: "" } }
// { site_id: "xxx", title: { ru: "c", de: "d"         }, content: { ru: "c" } }
  • The first part { site_id: "xxx" } is the match query, filtering which documents to update.

  • The second part [{ $set: { ... } }] is the update aggregation pipeline (note the squared brackets signifying the use of an aggregation pipeline). $set is a new aggregation operator and an alias of $addFields. The rest of this stage checks with $cond if the title.de exists, and if yes, then keep it as it is, or otherwise create it as an ''.

  • Don't forget { multi: true }, otherwise only the first matching document will be updated.

Solution 3:

there is a update field operator $setOnInsert which meets your requirement. Please read the documents here: https://docs.mongodb.com/manual/reference/operator/update/setOnInsert/#up._S_setOnInsert

Solution 4:

I have a solution for one particular case, but may be it helps someone.

My case was: Update several fields, among which was a field that had to be updated only once (lets call it "Date_of_first_update").

> db.test.find();
{ "_id" : ObjectId("57f298fdeb30478a033c70e4"), "a" : "1", "b" : "2" }

First update:

> db.test.updateOne({ "_id" : ObjectId("57f298fdeb30478a033c70e4")}, 
  {$set: {a: 100, b: 200 }, $min : {'Date_of_first_update' : (new Date())  }});

Result: 'a', 'b' updated, 'Date_of_first_update' is set.

{ "_id" : ObjectId("57f298fdeb30478a033c70e4"), "a" : 100, "b" : 200, "Date_of_first_update" : ISODate("2016-10-03T**17:47:43**.570Z") }

Second update:

> db.test.updateOne({ "_id" : ObjectId("57f298fdeb30478a033c70e4")}, 
  {$set: {a: 400, b: 800 }, $min : {'Date_of_first_update' : (new Date()) }});

Result: 'a', 'b' updated, 'Date_of_first_update' left unchanged, as I needed!!!

{ "_id" : ObjectId("57f298fdeb30478a033c70e4"), "a" : 400, "b" : 800, "Date_of_first_update" : ISODate("2016-10-03T**17:47:43**.570Z") } 

Solution 5:

If anyone encounters the problem as me: enter image description here

My solution is to set _id only if the update results in insert a new on (upsert = true) enter image description here