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
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.
$split
to break $keyPath
into an array containing the outer level field name prop
and inner field name nestedField
$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"
}
}
$mergeObjects
to merge the update object into $$ROOT
$replaceWith
to persist the updates$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"
}
])
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.