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"
}
]
]
}
}
}
}
}
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.
$splitto break$keyPathinto an array containing the outer level field namepropand inner field namenestedField- use
$arrayToObjecttwice to construct the update object with the help of$firstand$lastto access the outer and inner level field names. The update object should look like this:
{
"prop": {
"nestedField": "$val"
}
}
- use
$mergeObjectsto merge the update object into$$ROOT - use
$replaceWithto persist the updates - (optional)
$unsetthe helper fieldtokens
db.collection.update({},
[
{
"$set": {
"tokens": {
"$split": [
"$keyPath",
"."
]
}
}
},
{
"$replaceWith": {
"$mergeObjects": [
"$$ROOT",
{
"$arrayToObject": [
[
{
k: {
"$first": "$tokens"
},
v: {
"$arrayToObject": [
[
{
k: {
"$last": "$tokens"
},
v: "$val"
}
]
]
}
}
]
]
}
]
}
},
{
"$unset": "tokens"
}
])
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.