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中的嵌套条目,仅在子字段的最大值上求和和平均值