Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB: upsert sub-document

I have documents that looks something like that, with a unique index on bars.name:

{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }

. I want to either update the sub-document where { name: 'foo', 'bars.name': 'qux' } and $set: { 'bars.$.somefield': 2 }, or create a new sub-document with { name: 'qux', somefield: 2 } under { name: 'foo' }.

Is it possible to do this using a single query with upsert, or will I have to issue two separate ones?

Related: 'upsert' in an embedded document (suggests to change the schema to have the sub-document identifier as the key, but this is from two years ago and I'm wondering if there are better solutions now.)

like image 651
shesek Avatar asked May 05 '14 10:05

shesek


People also ask

Can a document have a sub document in MongoDB?

In Mongoose, subdocuments are documents that are nested in other documents. You can spot a subdocument when a schema is nested in another schema. Note: MongoDB calls subdocuments embedded documents.

Does MongoDB support Upsert?

Here in MongoDB, the upsert option is a Boolean value. Suppose the value is true and the documents match the specified query filter. In that case, the applied update operation will update the documents. If the value is true and no documents match the condition, this option inserts a new document into the collection.

How do I update a nested array in MongoDB?

Update Nested Arrays in Conjunction with $[]The $[<identifier>] filtered positional operator, in conjunction with the $[] all positional operator, can be used to update nested arrays. The following updates the values that are greater than or equal to 8 in the nested grades. questions array if the associated grades.


2 Answers

No there isn't really a better solution to this, so perhaps with an explanation.

Suppose you have a document in place that has the structure as you show:

{    "name": "foo",    "bars": [{         "name": "qux",         "somefield": 1    }]  } 

If you do an update like this

db.foo.update(     { "name": "foo", "bars.name": "qux" },     { "$set": { "bars.$.somefield": 2 } },     { "upsert": true } ) 

Then all is fine because matching document was found. But if you change the value of "bars.name":

db.foo.update(     { "name": "foo", "bars.name": "xyz" },     { "$set": { "bars.$.somefield": 2 } },     { "upsert": true } ) 

Then you will get a failure. The only thing that has really changed here is that in MongoDB 2.6 and above the error is a little more succinct:

WriteResult({     "nMatched" : 0,     "nUpserted" : 0,     "nModified" : 0,     "writeError" : {         "code" : 16836,         "errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"     } }) 

That is better in some ways, but you really do not want to "upsert" anyway. What you want to do is add the element to the array where the "name" does not currently exist.

So what you really want is the "result" from the update attempt without the "upsert" flag to see if any documents were affected:

db.foo.update(     { "name": "foo", "bars.name": "xyz" },     { "$set": { "bars.$.somefield": 2 } } ) 

Yielding in response:

WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 }) 

So when the modified documents are 0 then you know you want to issue the following update:

db.foo.update(     { "name": "foo" },     { "$push": { "bars": {         "name": "xyz",         "somefield": 2     }} ) 

There really is no other way to do exactly what you want. As the additions to the array are not strictly a "set" type of operation, you cannot use $addToSet combined with the "bulk update" functionality there, so that you can "cascade" your update requests.

In this case it seems like you need to check the result, or otherwise accept reading the whole document and checking whether to update or insert a new array element in code.

like image 129
Neil Lunn Avatar answered Sep 25 '22 10:09

Neil Lunn


if you dont mind changing the schema a bit and having a structure like so:

{ "name": "foo", "bars": { "qux": { "somefield": 1 },                            "xyz": { "somefield": 2 },                   } } 

You can perform your operations in one go. Reiterating 'upsert' in an embedded document for completeness

like image 20
nesdis Avatar answered Sep 22 '22 10:09

nesdis