MongoDB - 仅当嵌套数组中的所有条目存在时才更新它们

Posted

技术标签:

【中文标题】MongoDB - 仅当嵌套数组中的所有条目存在时才更新它们【英文标题】:MongoDB - Update all entries in nested array only if they exist 【发布时间】: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 是唯一的。实际上名称和路线不必相同,但为简单起见,我保持它们相同。 【参考方案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 错误)。衷心感谢您抽出宝贵的时间和出色的重新设计方法。 不客气 - 我已经更新了答案,以通过不同的设计进一步简化您的更新。我们现在可以使用聚合框架来填充引用。试一试,看看它是否适合您的用例。

以上是关于MongoDB - 仅当嵌套数组中的所有条目存在时才更新它们的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB $and 仅当 $and 在文档的同一数组索引中为真时才查询 - 特别是对于双重嵌套数组 [重复]

MongoDB/Mongoose - 仅当某个字段是唯一的时才将对象添加到对象数组中

mongodb中的嵌套条目,仅在子字段的最大值上求和和平均值

仅当数组列表尚不存在时才将其添加到数组列表中

如何在 mongodb/mongoose 的嵌套数组中检查是不是存在并更新对象?

mongodb $查找带有投影的数组中的嵌套对象