3

I have a use case where a view allows the user to update multiple objects and submit at once, how can I make this atomic?

{_id: parent,
 childrenA: [
   {_id: child1, property: "update-me", property2: "leave-alone"},
   {_id: child2, property: "leave-alone", property2: "update-me"}
 ],
 propertyA: "update-me",
 propertyB: "leave-alone", //someone else needs to be able to update this concurrently with this change.
 childrenB:[
   {property: "update-me", property2: "leave-alone"},
   {property: "leave-alone", property2: "update-me"}
 ],

}

property may or may not be another array of nested objects. Is any of this possible programmatically?

EDIT: I need to mention that I cannot reliably update the entire document in some cases, embedded documents can be replaced (address, maybe)

however, I need to aggregate a list of changes e.g. [{"child[Id=child1].FirstName", "newName"},{"child[Id=child3].LastName", "newName"} (not necessarily that syntax, but a change dictionary)

Chazt3n
  • 1,641
  • 3
  • 17
  • 42
  • Update of a single document (including its sub-documents) is atomic in Mongo DB. – Metheny May 12 '19 at 08:26
  • @Metheny - I hear that, but how can I specify multiple operations on field level changes to embedded documents living in arrays? I can't replace the whole document as multiple services may be using this document store backed service/ publishing events that this service listens to – Chazt3n May 12 '19 at 16:19
  • how about using the new transaction support starting in version 4.0? – Metheny May 12 '19 at 18:48
  • Thank you for the suggestion, I will be using a hosted document database that implements the Mongo APIs up to 3.6 – Chazt3n May 12 '19 at 19:45
  • 1
    Maybe this would help: https://docs.mongodb.com/manual/reference/operator/update/positional/ And as said any operation on a single document is considered atomic: https://stackoverflow.com/questions/21798432/atomicity-of-findandmodify-on-embedded-documents – Metheny May 13 '19 at 06:58
  • Thank you for continuing to support my question - evidently the positional operator can be used to updated only one of the above embedded documents – Chazt3n May 13 '19 at 18:40
  • 1
    Yes, you're right. Another attempt, it seems the following allows updating multiple array items according to a query: https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up._S_[] – Metheny May 13 '19 at 19:21
  • So that will update multiple items that match the same query - I need to go one step further and be able to update a property on an embedded document 123, a separate property on 321 but not change 456 – Chazt3n May 14 '19 at 19:19

2 Answers2

1

it could be done with one limitation to the best of my knowledge. someone correct me if i'm wrong please. here's the update command:

db.Parents.update(
    {
        "_id": ObjectId("5cf7391a1c86292244c4424e"),
        "ChildrenA": {
            "$elemMatch": {
                "_id": ObjectId("5cf7391a1c86292244c4424c")
            }
        }
    },
    {
        "$set": {
            "ChildrenA.$.Property": "UPDATED",
            "PropertyA": "UPDATED",
            "ChildrenB.0.Property": "UPDATED",
            "ChildrenB.1.Property2": "UPDATED"
        }
    }
)

as you can see you have to use $elemMatch to target a nested child by ID. and from what i can tell you can only have one $elemMatch in a single update command (correct me if i'm wrong).

here's the c# code that generated the above update command. it is using MongoDB.Entities which is a convenience library which i'm the author of.

using MongoDB.Entities;

namespace StackOverflow
{
    public class Program
    {
        public class Parent : Entity
        {
            public ChildA[] ChildrenA { get; set; }
            public string PropertyA { get; set; }
            public string PropertyB { get; set; }
            public ChildB[] ChildrenB { get; set; }
        }

        public class ChildA : Entity
        {
            public string Property { get; set; }
            public string Property2 { get; set; }
        }

        public class ChildB
        {
            public string Property { get; set; }
            public string Property2 { get; set; }
        }

        static void Main(string[] args)
        {
            new DB("test");

            var childA = new ChildA { Property = "update-me", Property2 = "leave-me-alone" };
            var childB = new ChildA { Property = "leave-alone", Property2 = "update-me" };
            childA.Save(); childB.Save();

            var parent = new Parent
            {
                ChildrenA = new[] { childA, childB },
                PropertyA = "update-me",
                PropertyB = "leave-me-alone",
                ChildrenB = new[] {
                new ChildB{ Property = "update-me", Property2 = "leave-me-alone"},
                new ChildB{ Property = "leave-alone", Property2 = "update-me"}
                }
            };
            parent.Save();

            DB.Update<Parent>()
              .Match(
                f => f.Eq(p => p.ID, parent.ID) &
                f.ElemMatch(
                    x => x.ChildrenA, 
                    x => x.ID == childA.ID))
              .Modify(x => x.ChildrenA[-1].Property, "UPDATED")
              .Modify(x => x.PropertyA, "UPDATED")
              .Modify(x => x.ChildrenB[0].Property, "UPDATED")
              .Modify(x => x.ChildrenB[1].Property2, "UPDATED")
              .Execute();
        }
    }
}
Dĵ ΝιΓΞΗΛψΚ
  • 5,068
  • 3
  • 13
  • 26
  • So essentially, if I know the location of the individual items, I can skip using $ at all and update using the .index notation? – Chazt3n Jun 05 '19 at 05:52
  • 1
    @Chazt3n yes that's right. if you know the index location of the items you want to update within an array you can batch up all of your changes in a single update command. the issue only arises when you need to target nested items by id or something else. when you need to do that, you gotta issue separate update commands for each item you need to filter on using $elemMatch (to the best of my knowledge). – Dĵ ΝιΓΞΗΛψΚ Jun 05 '19 at 09:04
  • That sounds like I can reliably pull the document, do all my searching via code, and queue up changes when I do this, will I be able to concurrently send updates to the same document or will this actually do a replace? – Chazt3n Jun 05 '19 at 13:39
  • 1
    @Chazt3n the update command with the [$set operator](https://docs.mongodb.com/manual/reference/operator/update/set/#up._S_set) only modifies the specified field. rest of the document is untouched. even my library uses the $set operator internally for updates. – Dĵ ΝιΓΞΗΛψΚ Jun 05 '19 at 15:51
1

You can use the following form version 3.4

db.Collection.findAndModify({
query: {  "_id" : "parent"},
update: { $set: {propertyA: "update-me" , "childrenA.$[childrenAelemnt].property" : "update-me" , "childrenB.$[childrenB2elemnt].property2" : "update-me"
},
arrayFilters: [ {"childrenAelemnt._id": "child1"},{"childrenBelemnt.property2": "leave-alone"} , {"childrenB2elemnt.property": "leave-alone"} ]})

If there is an ID field in childrenB array. it would have been little easier and syntax would have been little consistent

Roshan Vishva
  • 133
  • 1
  • 1
  • 5