mongodbaws-documentdb

How to safely update nested fields in DocumentDB when parent fields may be null, without overwriting existing data?


I am executing an update on a complex document in DocumentDB. I want to only update changed fields, without setting the object (and/or any nested objects) explicitly, to avoid overwriting existing data.

I am using TypeScript, and have written a function that rewrites the update to use dot notation for every field (to avoid overwriting objects).

The issue is that the document's fields may be null, so updating foo.bar.baz if foo or foo.bar are null.

Example documents in the collection:

{
  "foo": {
    "bar": {
      "baz": "myValue1"
    }
  }
}
{
  "foo": {
    "bar": null
  }
}
{ "foo": null }

Let's say for either one of these documents, I want to set foo.bar.qux to myValue2.

This is just an example - I want to have a way to handle updates to arbitrary fields at any level of nesting.

I have tried adding $ifNull to the update query:

db.collection.updateOne(
  { _id: 1 },
  {
    $set: {
      "foo": { $ifNull: ["$foo", {}] },
      "foo.bar": { $ifNull: ["$foo.bar", {}] },
      "foo.bar.qux": "myValue2",
    }
  }
)

That doesn't work - if foo is not null, then setting foo to $ifnull: ['$foo', {}] and then foo.bar in the same update fails, since it results in two updates to the foo.bar field in the same operation:

Cannot update 'foo.bar.qux' and 'foo.bar.qux' at the same time

The proper way to solve this would be an update using an aggregation pipeline using $mergeObjects, but DocumentDB does not support updates with an aggregation pipeline.

What are my options here? I want the solution to be as generic as possible and not require any knowledge about the schema or the data, so I could use it for all of my updates.


Solution

  • I would say the most canonical way would be using an aggregation to wrangle your data and iterate through the aggregation output to update back to the collection one by one. In this way you can leverage most of the supported aggregation API in AWS documentDB. Potentially if you are doing a lot of updates, you will want to wrap them in bulk operations too

    db.collection.aggregate([
      {
        "$set": {
          "foo.bar.qux": "value2"
        }
      }
    ]).forEach(function(result){
      db.collection.updateOne(
        {"_id": result._id}, 
        {"$set": {"foo": result.foo}}
      );
    })
    

    Mongo Playground to see the wrangle result