简体   繁体   中英

MongoDB - Update all entries in nested array only if they exist

I have a multilevel nested document (its dynamic and some levels can be missing but maximum 3 levels). I want to update all the children and subchildren routes if any. The scenario is same as in any Windows explorer, where all subfolders' route need to change when a parent folder route is changed. For eg. In the below example, If I am at route=="l1/l2a" and it's name needs to be edited to "l2c", then I will update it's route as route="l1/l2c and I will update all childrens' route to say "l1/l2c/l3a" .

     {
    "name":"l1",
    "route": "l1",
    "children":
        [
            {
            "name": "l2a",
            "route": "l1/l2a",
            "children": 
                [
                    {
                    "name": "l3a",
                    "route": "l1/l2a/l3a"
                 }]
            },
            {
            "name": "l2b",
            "route": "l1/l2b",
            "children": 
                [
                    {
                    "name": "l3b",
                    "route": "l1/l2b/l3b"
                 }]
            }
      ]
     }

Currently I am able to go to a point and I am able to change its name and ONLY its route in the following manner:

router.put('/navlist',(req,res,next)=>{
newname=req.body.newName //suppose l2c
oldname=req.body.name //suppose l2a
route=req.body.route // existing route is l1/l2a
id=req.body._id


newroute=route.replace(oldname,newname); // l1/l2a has to be changed to l1/l2c
let segments = route.split('/');  
let query = { route: segments[0]};
let update, options = {};

let updatePath = "";
options.arrayFilters = [];
for(let i = 0; i < segments.length  -1; i++){
    updatePath += `children.$[child${i}].`;
    options.arrayFilters.push({ [`child${i}.route`]: segments.slice(0, i + 2).join('/') });
} //this is basically for the nested children

updateName=updatePath+'name'
updateRoute=updatePath+'route';

update = { $setOnInsert: { [updateName]:newDisplayName,[updateRoute]:newroute } };      
NavItems.updateOne(query,update, options)
 })

The problem is I am not able to edit the routes of it's children if any ie it's subfolder route as l1/l2c/l3a . Although I tried using the $[] operator as follows.

updateChild = updatePath+'.children.$[].route'
updateChild2 = updatePath+'.children.$[].children.$[].route'
//update = { $set: { [updateChild]:'abc',[updateChild2]:'abc' } };

Its important that levels are customizable and thus I don't know whether there is "l3A" or not. Like there can be "l3A" but there may not be "l3B". But my code simply requires every correct path else it gives an error

code 500 MongoError: The path 'children.1.children' must exist in the document in order to apply array updates.

So the question is how can I apply changes using $set to a path that actually exists and how can I edit the existing route part. If the path exists, it's well and good and if the path does not exist, I am getting the ERROR.

Update

You could simplify updates when you use references.Updates/Inserts are straightforward as you can only the update target level or insert new level without worrying about updating all levels. Let the aggregation takes care of populating all levels and generating route field.

Working example - https://mongoplayground.net/p/TKMsvpkbBMn

Structure

[
  {
    "_id": 1,
    "name": "l1",
    "children": [
      2,
      3
    ]
  },
  {
    "_id": 2,
    "name": "l2a",
    "children": [
      4
    ]
  },
  {
    "_id": 3,
    "name": "l2b",
    "children": [
      5
    ]
  },
  {
    "_id": 4,
    "name": "l3a",
    "children": []
  },
  {
    "_id": 5,
    "name": "l3b",
    "children": []
  }

]

Insert query

db.collection.insert({"_id": 4, "name": "l3a", "children": []}); // Inserting empty array simplifies aggregation query 

Update query

db.collection.update({"_id": 4}, {"$set": "name": "l3c"});

Aggregation

db.collection.aggregate([
  {"$match":{"_id":1}},
  {"$lookup":{
    "from":"collection",
    "let":{"name":"$name","children":"$children"},
    "pipeline":[
      {"$match":{"$expr":{"$in":["$_id","$$children"]}}},
      {"$addFields":{"route":{"$concat":["$$name","/","$name"]}}},
      {"$lookup":{
        "from":"collection",
        "let":{"route":"$route","children":"$children"},
        "pipeline":[
          {"$match":{"$expr":{"$in":["$_id","$$children"]}}},
          {"$addFields":{"route":{"$concat":["$$route","/","$name"]}}}
        ],
        "as":"children"
      }}
    ],
    "as":"children"
  }}
])

Original

You could make route as array type and format before presenting it to user. It will greatly simplify updates for you. You have to break queries into multiple updates when nested levels don't exist ( ex level 2 update ). May be use transactions to perform multiple updates in atomic way.

Something like

[
  {
    "_id": 1,
    "name": "l1",
    "route": "l1",
    "children": [
      {
        "name": "l2a",
        "route": [
          "l1",
          "l2a"
        ],
        "children": [
          {
            "name": "l3a",
            "route": [
              "l1",
              "l2a",
              "l3a"
            ]
          }
        ]
      }
    ]
  }
]

level 1 update

db.collection.update({
  "_id": 1
},
{
  "$set": {
    "name": "m1",
    "route": "m1"
  },
  "$set": {
    "children.$[].route.0": "m1",
    "children.$[].children.$[].route.0": "m1"
  }
})

level 2 update

db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[child].route.1": "m2a",
    "children.$[child].name": "m2a"
  }
},
{
  "arrayFilters":[{"child.name": "l2a" }]
})


db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[child].children.$[].route.1": "m2a"
  }
},
{
  "arrayFilters":[{"child.name": "l2a"}]
})

level 3 update

db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[].children.$[child].name": "m3a"
    "children.$[].children.$[child].route.2": "m3a"
  }
},
{
  "arrayFilters":[{"child.name": "l3a"}]
})

you can't do as you want. Because mongo does not support it. I can offer you to fetch needed item from mongo. Update him with your custom recursive function help. And do db.collection.updateOne(_id, { $set: data })

function updateRouteRecursive(item) {
  // case when need to stop our recursive function
  if (!item.children) {
    // do update item route and return modified item
    return item;
  }

  // case what happen when we have children on each children array
}

I don't think its possible with arrayFilted for first level and second level update, but yes its possible only for third level update,

The possible way is you can use update with aggregation pipeline starting from MongoDB 4.2 ,

I am just suggesting a method, you can simplify more on this and reduce query as per your understanding!

Use $map to iterate the loop of children array and check condition using $cond , and merge objects using $mergeObjects ,

let id = req.body._id;
let oldname = req.body.name;
let route = req.body.route;
let newname = req.body.newName;

let segments = route.split('/');

LEVEL 1 UPDATE: Playground

// LEVEL 1: Example Values in variables
// let oldname = "l1";
// let route = "l1";
// let newname = "l4";
if(segments.length === 1) {
  let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                name: newname,
                route: newname,
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    route: { $concat: [newname, "/", "$$a2.name"] },
                                    children: {
                                        $map: {
                                            input: "$$a2.children",
                                            as: "a3",
                                            in: {
                                                $mergeObjects: [
                                                    "$$a3",
                                                    { route: { $concat: [newname, "/", "$$a2.name", "/", "$$a3.name"] } }
                                                ]
                                            }
                                        }
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}

LEVEL 2 UPDATE: Playground

// LEVEL 2: Example Values in variables
// let oldname = "l2a";
// let route = "l1/l2a";
// let newname = "l2g";
else if (segments.length === 2) {
    let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    $cond: [
                                        { $eq: ["$$a2.name", oldname] },
                                        {
                                            name: newname,
                                            route: { $concat: ["$name", "/", newname] },
                                            children: {
                                                $map: {
                                                    input: "$$a2.children",
                                                    as: "a3",
                                                    in: {
                                                        $mergeObjects: [
                                                            "$$a3",
                                                            { route: { $concat: ["$name", "/", newname, "/", "$$a3.name"] } }
                                                        ]
                                                    }
                                                }
                                            }
                                        },
                                        {}
                                    ]
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}

LEVEL 3 UPDATE: Playground

// LEVEL 3 Example Values in variables
// let oldname = "l3a";
// let route = "l1/l2a/l3a";
// let newname = "l3g";
else if (segments.length === 3) {
    let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    $cond: [
                                        { $eq: ["$$a2.name", segments[1]] },
                                        {
                                            children: {
                                                $map: {
                                                    input: "$$a2.children",
                                                    as: "a3",
                                                    in: {
                                                        $mergeObjects: [
                                                            "$$a3",
                                                            {
                                                                $cond: [
                                                                    { $eq: ["$$a3.name", oldname] },
                                                                    {
                                                                        name: newname,
                                                                        route: { $concat: ["$name", "/", "$$a2.name", "/", newname] }
                                                                    },
                                                                    {}
                                                                ]
                                                            }
                                                        ]
                                                    }
                                                }
                                            }
                                        },
                                        {}
                                    ]
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}

Why separate query for each level?

You could do single query but it will update all level's data whenever you just need to update single level data or particular level's data, I know this is lengthy code and queries but i can say this is optimized version for query operation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM