75

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.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Miraage
  • 3,334
  • 3
  • 26
  • 43
  • Actually this will not overwrite and would in fact "add" the new key to the "first" document only since you set [multi](http://docs.mongodb.org/manual/reference/method/db.collection.update/#multi-parameter). But you knew that didn't you? – Neil Lunn Jul 18 '14 at 12:21
  • Ofc I know about multi and I'm using it in my query. But I need to something like `$add`, not `$set`. – Miraage Jul 18 '14 at 12:22
  • Would you mind to elaborate your example? Are your searching for something like $push or $addToSet? – nutlike Jul 18 '14 at 12:32

9 Answers9

87

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})
I159
  • 29,741
  • 31
  • 97
  • 132
nutlike
  • 4,835
  • 1
  • 25
  • 33
  • 20
    I wanted to do it in single query. – Miraage Jul 18 '14 at 12:54
  • Just to info, if you are PHP developer, you can read more here about the samples: http://php.net/manual/en/mongocollection.update.php – ParisNakitaKejser May 26 '16 at 12:43
  • 1
    if wants to update to key in same raw then .update({'qwerty':{$exists:false}, 'content': {$exists : false}}, {$set: {'content': '','qwerty':''}}, {multi: true}) – Nitin Sep 15 '17 at 11:30
  • 3
    @Nitin, I think there's a problem with this multi-statement; it will only update documents where **all** of the specified fields do not exists. It's possible to use `$or` here, but then all fields will be overwritten in any document that one of them does not exists. I'm also trying to find a "one operation" solution to this... – Yuval A. Oct 27 '17 at 19:37
33

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.updateMany(
  { site_id: "xxx" },
  [{ $set: {
    "title.de": { $cond: [ { $not: ["$title.de"] }, "", "$title.de" ] },
    "content.ru": { $cond: [ { $not: ["$content.ru"] }, "", "$content.ru" ] }
  }}]
)
// { 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 ''.

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • 3
    Thanks! Just what I was looking for. – 010011100101 Sep 07 '19 at 10:03
  • Minor addition: there is an operator called `$ifNull` (https://www.mongodb.com/docs/manual/reference/operator/aggregation/ifNull/) which is created specifically for matching undefined and missing fields. – esimonov Aug 15 '23 at 09:49
23

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

Bing Wu
  • 389
  • 3
  • 4
  • Perfect! I did not know this exists. This seem to be exactly what OP was looking for – freeall Nov 18 '16 at 13:12
  • 30
    Note: this does not work if you want to set new fields for existing documents. setOnInsert does nothing if a new document was not inserted during an upsert – Aarjav Nov 21 '16 at 17:06
  • @Aarjav I completely agree - I actually need the functionality you just mentioned, which I believe the OP was looking for. Do you know of a way to accomplish it? – Corbfon Jul 15 '17 at 18:21
  • @Corbfon Unfortunately I don't think there is a way around multiple update operations. A possible but less than elegant work around might be to use the $min or $max operators but they probably won't work in most situations. https://docs.mongodb.com/manual/reference/bson-type-comparison-order/#faq-dev-compare-order-for-bson-types – Aarjav Jul 16 '17 at 00:46
  • @Aarjav disappointing :( I'll just make a single call to get the document in question, have the service logically update the proper fields and then save the thing all at once. 2 db ops vs > 5 – Corbfon Jul 20 '17 at 13:24
17

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") } 
hi_artem
  • 259
  • 3
  • 8
6

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)

return {
    updateOne: {
        filter: {
            email: shadowUser.email,
        },
        update: {
            $set: user,
            $setOnInsert: { _id: shadowUser._id },
        },
        upsert: true,
    },
};
Throvn
  • 795
  • 7
  • 19
davychhouk
  • 1,549
  • 2
  • 16
  • 20
3

Even though the answer given essentially outlines the approach, you can do this sort of thing with MongoDB 2.6 or greater due to the implementation there that supports "bulk updates".

This are still, "atomically speaking", separate update statements. But you can submit them "over the wire" in one go. Which at least makes sure that the latency between the updates is much shorter as they are executed on the server:

var bulk = db.foo.initializeBulkOrderedOp();
bulk.find({ "site_id": "xxx",
    "title.de": { "$exists" false } })
    .update({ "$set": { "title.de": "" } });
bulk.find({ "site_id": "xxx", 
    "content.de": { "$exists" false } })
    .update({ "$set": { "content.de": "" } });
bulk.execute();

So that is actually one round trip to the server as everything only sends on .execute()

But in your present form (though this may not be a accurate representation of your data), you can actually "re-structure" in order to do this in a single operation. So if your documents looked like this:

{ 
    "site_id": "xxx",
    "docs": [
        { "title": "a", "content": "a", "lang": "ru" },
        { "title": "b", "content": "b", "lang": "en" }
    ]
},
{
    "site_id": "xxx",
    "docs": [
        { "title": "c", "content": "c", "lang": "ru" },
        { "title": "d", "content": "d", "lang": "de" }
    ]
}

Then the following works by the rule of $addToSet where the "set" element would be "unique":

db.foo.update(
    { "site_id": "xxx" },
    { "$addToSet": { "docs": { "title": "d", content: "d", "lang": "de" } } },
    { "multi": true }
)

Or even without the logic there and just checking for presence:

db.foo.update(
    { "site_id": "xxx", "docs.lang": { "$ne": "de" } },
    { "$push": { "docs": { "title": "", "content": "", "lang": "de" } } },
    { "multi": true }

)

Which in that last case would result in this:

{
    "_id" : ObjectId("53c936265117367f5ff2038b"),
    "site_id" : "xxx",
    "docs" : [
            {
                    "title" : "a",
                    "content" : "a",
                    "lang" : "ru"
            },
            {
                    "title" : "b",
                    "content" : "b",
                    "lang" : "en"
            },
            {
                    "title" : "",
                    "content" : "",
                    "lang" : "de"
            }
    ]
}
{
    "_id" : ObjectId("53c936265117367f5ff2038c"),
    "site_id" : "xxx",
    "docs" : [
            {
                    "title" : "c",
                    "content" : "c",
                    "lang" : "ru"
            },
            {
                    "title" : "d",
                    "content" : "d",
                    "lang" : "de"
            }
    ]
}

So the choice is there to either "handle" things differently or otherwise just change your schema to accommodate the sort of updates you want to do atomically.

Yoshiya
  • 452
  • 5
  • 17
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
2

@nutlike's answer does solve the issue, however, if you would like to update multiple fields on the item it will require many database operations. In short, what you want is not exactly possible.

If your doc has more updates to do than you'd like to do one at a time (any greater than 2, IMO), then you should just get the document, update the fields and then save it. This is what I do on a couple of OAuth user creating/updating routes.

Corbfon
  • 3,514
  • 1
  • 13
  • 24
2

As update query is deprecated use updateOne or updateMany as per your requirement.

updateMany to update all at once.

db.foo.updateMany({'title.de': {$exists : false}}, {$set: {'title.de': ''}})
Amit Chhatbar
  • 93
  • 1
  • 9
2

// Add a field with some values in all documents if the field does not exist, Here, I am saying that adding lob: 'Marine' if the lob field is not there in all documents.

db.collections.updateMany({'lob': { $exists: false }}, { $set: { lob: 'Marine'}});