arrays

c#

mongodb

mongodb-query

my db model looks like this

"clientId":"123456"
"devices" : 
[{
      "deviceId" : "123",
      "deviceType" : "ios",
      "notification" : true,
  }
  {
      "deviceId" : "321",
      "deviceType" : "android",
      "notification" : true,

  }
 ]

The c# model I pass to my DB method has ClientId,DeviceId,DeviceType,notification. Note that it doesn't have an array of devices

the behaviour I'm trying to achieve

  1. (working) If ClientId is not yet in the database, create a new record in db with the passed value,(devices array will have one element)

  2. If ClientId is already found in the database, update the the record according to the following rules:

    2.1 If devices array doesn't contain DeviceId add a new element to the array with the passed values

    2.2 If devices array already contains DeviceId update the element's deviceType and notification.

I'm using c#. any help appreciated

example: given the above state of database, by passing clientId: 123456, deviceId 321 deviceType "kindle", notification "false" the db will change to

"clientId":"123456"
"devices" : 
[{
      "deviceId" : "123",
      "deviceType" : "ios",
      "notification" : true,
  }
  {
      "deviceId" : "321",
      "deviceType" : "kindle",
      "notification" : false,

  }
 ]

Solution 1

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

Solution 2

You're looking for a typical positional operator case:

db.coll.update
(
    {
        "clientId":"123456","devices.deviceId":"321"
    },
    {
        $set:
        {
            "devices.$.deviceType":"kindle","devices.$.notification":"false"
        }
    }
)

You need to update an element matched inside an array, $ does this for you, so you can find a document with a specific element inside of an array and "remember" with the positional operator $ which one you matched to update it.