This is not really as simple as you might think, and actually it is interesting that you have broken your analysis of this into three parts. Because, guess what? That is exactly what you must do. Let's consider the steps:
1. Insert a document if one doesn't exist
db.collection.update(
{
"clientId":"123456"
},
{
"$setOnInsert": {
"clientId": "123456",
"devices": [{
"deviceId": "321",
"deviceType" : "kindle",
"notification" : false
}]
}
},
{ "upsert": true }
)
So what you want to do is insert a new document where the "clientId" currently does not exist. This can be done as an "upsert" to avoid possible unique key clashes and even where there is no "unique" constraint, then the "upsert" nature of this ensures you only create the "new" document when it was not found. Also there is $setOnInsert
here because you do not want to do anything to a document that is "found" at this point.
Note here that there is no attempt to match the element in the array. This is because you likely do not want to "create" a new document just because an existing one did not have "that" array element. Which brings us to the next step.
2. Update the contents of the document where it does exist
db.collection.update(
{
"clientId":"123456",
"devices": { "$elemMatch": { "deviceId" : "321" } }
},
{
"$set": {
"devices.$.deviceType" : "kindle",
"devices.$.notification" : false
}
}
)
Now here you want to actually try and "match" the document for the "clientId" that does contain an element in the array that also matches the "deviceId" you are looking for. So by specifying a condition to match, you get the use of the positional $
operator in order to set the fields in the "matching" position.
As above, this was either going to match one thing or nothing so either the update was done or it wasn't. So that moves to our final part of the cascade here:
3. Add the array element where it does not exist
db.collection.update(
{
"clientId":"123456"
},
{
"$addToset": { "devices": {
"deviceId" : "321",
"deviceType" : "kindle",
"notification" : false
}}
}
)
So this is importantly the last stage. The reason being that if either of the preceding operations did "create" or "update" the existing document, then the use of $addToSet
here makes sure you are not "pushing" another document to the array with the same "deviceId" but other different values. If one of those stages worked, then this would see all the values of that element to already exist, and would not then add another one.
If you tried to do that in a different order, in the case you present you would have two documents in the array with the same "deviceId", but differing values for "deviceType" and "notification". So that is why it comes last.
Conclusion
So unfortunately, there is no simple way to combine these as one operation. The operators simply do not exist so that this could be done in a single statement and therefore you must perform three update operations in order to do what you want. Also as stated, the order of application for those updates is important so that you get the desired result.
While this does not exist yet in current "production" releases, the upcoming release ( 2.6 and upwards as of writing) does have a way to to "batch" these requests with an new syntax to update:
db.runCommand(
"update": "collection",
"updates": [
{
"q": { "clientId":"123456" },
"u": {
"$setOnInsert": {
"clientId": "123456",
"devices": [{
"deviceId": "321",
"deviceType" : "kindle",
"notification" : false
}]
},
"upsert": true
},
{
"q": {
"clientId":"123456",
"devices": { "$elemMatch": { "deviceId" : "321" } }
},
"u": {
"$set": {
"devices.$.deviceType" : "kindle",
"devices.$.notification" : false
}
}
},
{
"q": { "clientId":"123456" },
"u": {
"$addToset": { "devices": {
"deviceId" : "321",
"deviceType" : "kindle",
"notification" : false
}}
}
}
]
)
So while that is still essentially three operations, at least you get to send them over the wire just once