mongodbaggregation-framework

How to update the value of a nested field with dynamic name in MongoDB aggregation pipeline?


During MongoDB aggregation I need to update the value of a nested field, but the name of that field is the value of another field. Example document:

{
  "prop": {
    "nestedField": "oldValue"       // Property to be updated
  },   
  "keyPath": "prop.nestedField",    // Path to the target field using dot notation
  "val": "newValue"                 // New value to set to target field
}

so after aggregation it should become:

{
  "prop": {
    "nestedField": "newValue"       // Value has been updated
  },   
  ...         
}

The path is guaranteed to be present in the document.

I tried using $arrayToObject, but for nested key paths it interprets the values as literals and creates fields with exact values of key path (like "prop.nestedField") and not nested object.

Sure, this can be done after aggregation, but following stages need to use the updated data, so I'm interested in doing it inside the aggregation.

Been struggling with this for a few days, would really appreciate some help


Solution

  • Edit: figured out a convoluted way to do for more than 1 layer of nesting. The idea would be similar: construct an update object and $mergeObject to $$ROOT. The update object can be dynamically constructed with $reduce.

    {
      "$set": {
        "updateObject": {
          "$reduce": {
            "input": {
              "$slice": [
                "$tokens",
                1,
                {
                  "$size": "$tokens"
                }
              ]
            },
            "initialValue": {
              "$arrayToObject": [
                [
                  {
                    k: {
                      "$first": "$tokens"
                    },
                    v: "$val"
                  }
                ]
              ]
            },
            "in": {
              "$arrayToObject": [
                [
                  {
                    k: "$$this",
                    v: "$$value"
                  }
                ]
              ]
            }
          }
        }
      }
    }
    

    full code in Mongo Playground


    Highly nested documents and using dynamic values as field names are considered as anti-patterns and should be avoided. Consider refactoring your schema if possible.

    However for your current scenario, if the value in keyPath is only nested 1 layer, it can still be manageable by dynamically construct the update object using $arrayToObject and use $mergeObjects to merge it to the $$ROOT object.

    1. $split to break $keyPath into an array containing the outer level field name prop and inner field name nestedField
    2. use $arrayToObject twice to construct the update object with the help of $first and $last to access the outer and inner level field names. The update object should look like this:
    {
        "prop": {
          "nestedField": "$val"
        } 
    }
    
    1. use $mergeObjects to merge the update object into $$ROOT
    2. use $replaceWith to persist the updates
    3. (optional) $unset the helper field tokens
    db.collection.update({},
    [
      {
        "$set": {
          "tokens": {
            "$split": [
              "$keyPath",
              "."
            ]
          }
        }
      },
      {
        "$replaceWith": {
          "$mergeObjects": [
            "$$ROOT",
            {
              "$arrayToObject": [
                [
                  {
                    k: {
                      "$first": "$tokens"
                    },
                    v: {
                      "$arrayToObject": [
                        [
                          {
                            k: {
                              "$last": "$tokens"
                            },
                            v: "$val"
                          }
                        ]
                      ]
                    }
                  }
                ]
              ]
            }
          ]
        }
      },
      {
        "$unset": "tokens"
      }
    ])
    

    Mongo Playground


    Note: If your keyPath is nested more than once, this solution is unlikely to be extendable to construct a dynamic level of update object, and thus cannot be applied. Even if it is possible to do so, it would introduce a lot of code smell and make the query unmaintainable. Again, it is advised to refactor the schema if possible.