【问题标题】:MongoDB - Update all entries in nested array only if they existMongoDB - 仅当嵌套数组中的所有条目存在时才更新它们
【发布时间】:2021-02-20 06:28:09
【问题描述】:

我有一个多级嵌套文档(它的动态和某些级别可能会丢失,但最多 3 个级别)。如果有的话,我想更新所有的子路由和子路由。该方案与任何 Windows 资源管理器中的情况相同,其中所有子文件夹的路由都需要在父文件夹路由发生更改时更改。例如。在下面的例子中,如果我在route=="l1/l2a"并且它的名字需要编辑为“l2c”,那么我将它的路线更新为route="l1/l2c,我将更新所有孩子的路线说"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"
                 }]
            }
      ]
     }

目前我可以去一个点,我可以通过以下方式更改它的名称和路线:

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)
 })

问题是我无法编辑它的孩子的路线(如果有的话),即它的子文件夹路线为l1/l2c/l3a。虽然我尝试如下使用$[] 运算符。

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

关卡可自定义很重要,因此我不知道是否有“l3A”。就像可以有“l3A”但可能没有“l3B”。但我的代码只需要每条正确的路径,否则会出错

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

所以问题是如何使用 $set 将更改应用到实际存在的路径以及如何编辑现有的路径部分。如果路径存在,那很好,如果路径不存在,我会收到错误消息。

【问题讨论】:

  • route:l1 是唯一的吗?
  • 是的 l1 是唯一的。实际上名称和路线不必相同,但为简单起见,我保持它们相同。

标签: javascript node.js mongodb mongoose nosql


【解决方案1】:

你不能随心所欲。因为mongo不支持。我可以让您从 mongo 获取所需的物品。使用您的自定义递归函数帮助更新他。并做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
}

【讨论】:

    【解决方案2】:

    我认为arrayFilted 不可能用于第一级和第二级更新,但是是的,它只能用于第三级更新,

    可能的方法是您可以从 MongoDB 4.2 开始使用update with aggregation pipeline

    我只是建议一种方法,您可以根据自己的理解对此进行更多简化并减少查询!

    使用$map迭代children数组的循环,使用$cond检查条件,使用$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('/');
    

    1 级更新: 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"] } }
                                                    ]
                                                }
                                            }
                                        }
                                    }
                                ]
                            }
                        }
                    }
                }
            }]
        );
    }
    

    2 级更新: 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"] } }
                                                            ]
                                                        }
                                                    }
                                                }
                                            },
                                            {}
                                        ]
                                    }
                                ]
                            }
                        }
                    }
                }
            }]
        );
    }
    

    3 级更新: 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] }
                                                                        },
                                                                        {}
                                                                    ]
                                                                }
                                                            ]
                                                        }
                                                    }
                                                }
                                            },
                                            {}
                                        ]
                                    }
                                ]
                            }
                        }
                    }
                }
            }]
        );
    }
    

    为什么要为每个级别单独查询?

    您可以进行单个查询,但只要您只需要更新单个级别数据或特定级别的数据,它就会更新所有级别的数据,我知道这是冗长的代码和查询,但我可以说这是查询操作的优化版本。

    【讨论】:

    • 感谢您根据场景提供恰当的答案。在这里和那里进行了一些小调整 :) 使用地图迭代文档的方式对我来说是新事物。很高兴获得赏金。
    【解决方案3】:

    更新

    您可以在使用引用时简化更新。更新/插入很简单,因为您只能更新目标级别或插入新级别,而无需担心更新所有级别。让聚合负责填充所有级别并生成路由字段。

    工作示例 - https://mongoplayground.net/p/TKMsvpkbBMn

    结构

    [
      {
        "_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": []
      }
    

    ]

    插入查询

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

    更新查询

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

    聚合

    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"
      }}
    ])
    

    原创

    您可以在将路由呈现给用户之前将其设置为数组类型和格式。它将为您大大简化更新。当嵌套级别不存在时,您必须将查询分成多个更新(例如 2 级更新)。可以使用事务以原子方式执行多次更新。

    类似

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

    1 级更新

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

    2 级更新

    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"}]
    })
    

    3 级更新

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

    【讨论】:

    • 儿童版的更新同样出现 500 错误,可能无法解决我的主要问题。例如。在 level1 更新中, "children.$[].children.$[].route.0": "m1" 创建 500 mongo 错误。测试用例是 "children.$[].children.$[].route.0" 不存在时。但是@s7vr 将路由划分为数组的想法很棒。当我编辑项目的预先存在的值并消除首先读取值的需要时,它无疑会带来更多的清晰度。虽然看起来整体更新执行可能需要类似的努力(关于 500 错误)。衷心感谢您抽出宝贵的时间和出色的重新设计方法。
    • 不客气 - 我已经更新了答案,以通过不同的设计进一步简化您的更新。我们现在可以使用聚合框架来填充引用。试一试,看看它是否适合您的用例。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-14
    • 2020-12-24
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 2020-10-15
    相关资源
    最近更新 更多