Cosmos DB Patch child object
Asked Answered
O

2

13

I'm trying to patch a child object of a Cosmos Db document using the new Partial Document Update feature. However, I can't find a way to do it.

If the document looks like below, how would you update child (in the Items array) with ID 2 ProductCode property to "A-111" using the new Patch method?


{
    "id": "SalesOrder2",
    "ponumber": "PO15428132599",
    "OrderDate": "2005-07-01T00:00:00",
    "DueDate": "2005-07-13T00:00:00",
    "ShippedDate": "2005-07-08T00:00:00",
    "AccountNumber": "Account2",
    "SubTotal": 6107.082,
    "TaxAmt": 586.1203,
    "Freight": 183.1626,
    "TotalDue": 4893.3929,
    "DiscountAmt": 1982.872,
    "Items": [
        {
            "Id": 1,
            "OrderQty": 3,
            "ProductCode": "A-123",
            "ProductName": "Product 1",
            "CurrencySymbol": "$",
            "CurrencyCode": "USD",
            "UnitPrice": 17.1,
            "LineTotal": 5.7
        },
        {
            "Id": 2,
            "OrderQty": 2,
            "ProductCode": "A-456",
            "ProductName": "Product 2",
            "CurrencySymbol": "$",
            "CurrencyCode": "USD",
            "UnitPrice": 10,
            "LineTotal": 20
        }
    ],
    "_rid": "BsMkAMc43s4CAAAAAAAAAA==",
    "_self": "dbs/BsMkAA==/colls/BsMkAMc43s4=/docs/BsMkAMc43s4CAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-e136-0dbec04601d7\"",
    "_attachments": "attachments/",
    "_ts": 1637760030
}

Olivero answered 24/11, 2021 at 13:32 Comment(2)
are you using Dotnet or Java or Nodejs?Phytophagous
I'm using DotnetOlivero
P
12

You need to use the Replace Operation by passing the specific index of the object that you want to update,

in this case, it would be something like,

 ItemResponse<SalesOrder> response = await container.PatchItemAsync<SalesOrder>(
 id: "SalesOrder2",
 partitionKey: new PartitionKey("/SalesOrder2"),
 patchOperations: new[] { PatchOperation.Replace("/Items/1/ProductCode","A-111") });
Phytophagous answered 24/11, 2021 at 17:28 Comment(7)
Is there a way to do this without specifying the index position (in this case, 1) of the element? For example, the OP said, "with ID 2". Could this be done using the ID instead pf the array position?Hayrick
Sajeetharan gets us most of the way. One important note is that you need to know the index of the child item array you want to update. So the only way I could think of doing that is querying the document for the child array you want to update, use C# to find the matching index and then use it in his patch code example.Environs
@TheMuffinMan - I'm new in noSQL, so want to ask - is there any other noSQL database service that I can use instead of CosmosDB to avoid mentioned issue with array index? I'm interested in both cases: on-cloud and on-premise solutions. Thank you.Waddell
I don't think so. You either need to know the index up front, query the entire array to get the index or model your data differently for your use case. Remember that denormalization is normal in noSQL. You have to make decisions based on your use case such as heavy read or writes. Write additional logic to keep your data sets in syncEnvirons
@Hayrick maybe a filter predicate? learn.microsoft.com/en-au/azure/cosmos-db/…Sloth
@Sloth see this SO question for my attempts to use the filter predicate to resolve this without an index. (tldr: the filter predicate seems to be an all or nothing. If the filter condition passes, then the operation continues. If not, then it cancels.)Hayrick
Please upvote feedback.azure.com/d365community/idea/… if you would like the ability to perform partial document updates on array elements without specifying indexes explicitly.Menstruation
E
1

Unfortunately, there isn't a way to use some sql like syntax to update a specific child array item using a condition. You have to know the index to it. That means you need to pull down your document (you can select only the fields you need to do this work).

This is verbose, but it's really not doing a whole lot once you read through it.

Document:

{
"id": "15bab994-6ea2-436c-badd-a31f44d2e85d",
"conversationId": "5663d3ff-1347-4413-a584-9c634425c7ab",
"subject": "Office credit card",
"dateSent": "2022-06-08",
"dateCreated": "2022-06-06",
"isRetracted": false,
"parentId": "",
"isDeletedForSender": true,
"sender": {
    "emailAddress": "[email protected]",
    "isUnread": true
},
"recipients": [
    {
        "id": 2,
        "emailAddress": "[email protected]",
        "type": 2,
        "isUnread": true,
        "labelIds": [
            2
        ]
    },
    {
        "id": 1,
        "emailAddress": "[email protected]",
        "type": 1,
        "isUnread": true,
        "labelIds": [
            1
        ]
    }
],
"attachments": [
    {
        "name": "card.pdf",
        "sizeInBytes": 129323
    }
],
"_rid": "zl9jANa86aABAAAAAAAAAA==",
"_self": "dbs/zl9jAA==/colls/zl9jANa86aA=/docs/zl9jANa86aABAAAAAAAAAA==/",
"_etag": "\"0d0093b8-0000-0100-0000-62a57c470000\"",
"_attachments": "attachments/",
"recipients[0].isUnread": true,
"_ts": 1655012423
}

Conditional patch code to update isUnread for sender or recipients that match a specific email.

var container = _cosmosClient.GetContainer("my-database", "Message");

    // Get all of the messages that match the list of conversations to update
    var queryResultSetIterator = container.GetItemLinqQueryable<Message2>()
        .Select(x => new PatchMessageContainer
        {
            Id = x.Id,
            ConversationId = x.ConversationId,
            Recipients = x.Recipients,
            Sender = x.Sender
        })
        .Where(x => payload.ConversationIds.Contains(x.ConversationId)).ToFeedIterator();
    var conversations = new List<PatchMessageContainer>();
    while (queryResultSetIterator.HasMoreResults)
        conversations.AddRange(await queryResultSetIterator.ReadNextAsync().ConfigureAwait(false));

    // Set isUnread for the authenticated user in the recipients list
    var updateTasks = new List<Task>();
    foreach (var conversation in conversations)
    {
        // The following Patch operations are smart enough to skip if the target value is already set to the desired value
        var patchOpts = new List<PatchOperation>();
        // Update sender isUnread if sender matches auth email
        if (conversation.Sender.EmailAddress == userClaims.Email && conversation.Sender.IsUnread != payload.SetIsUnread)
            patchOpts.Add(PatchOperation.Set($"/sender/isUnread", payload.SetIsUnread));

        // Update recipient isUnread where recipient matches auth email
        var idx = conversation.Recipients.FindIndex(x => x.EmailAddress == userClaims.Email);
        if (idx > -1 && conversation.Recipients[idx].IsUnread != payload.SetIsUnread)
            patchOpts.Add(PatchOperation.Set($"/recipients/{idx}/isUnread", payload.SetIsUnread));

        // Send the actual patch request
        var task = container.PatchItemAsync<Message2>(conversation.Id.ToString(),
               new PartitionKey(conversation.ConversationId.ToString()), patchOpts);
        updateTasks.Add(task);
    }

    await Task.WhenAll(updateTasks);

conversations variable here is really just the Message document in the first code block, but I've only selected a subset of fields needed to accomplish the update.

Hope this helps.

Environs answered 12/6, 2022 at 5:50 Comment(3)
I sure how Azure updates this soon, not being able to specify a more precise filter query to child arrays to specify what to patch. If I load an object with an array of 10 items and issue a PatchItemAsync() for the item at index 2, someone else could have inserted a new array item at the start and my patch would update the wrong item. No exception gets raised... just a hard to track down runtime bug.Teran
@BenjaminBrandt I share the same frustrations but I think right approach here is verify the etag as means of checking "versioning" of the document you are patching. Cosmos SDK has built in support for that and you'd then handle those cases in your code either refusing update or implementing some sort of retry mechanism.Alyose
To me, it's a bit of a bummer to need to write your own code to validate the etag for this case. MongoDB includes mongodb.com/docs/manual/reference/operator/update/positional to allow for this. See feedback.azure.com/d365community/idea/… if you want similar functionality added to the SQL api.Menstruation

© 2022 - 2024 — McMap. All rights reserved.