如何用MongoDB $graphLookup实现文档层级关系查询?
基于MongoDB $graphLookup实现双向层级关系查询
核心问题分析
你的需求是通过partOf(子节点关联父节点ID)和contains(父节点关联子节点ID)两个数组字段,查询文档的完整层级关系。普通单方向的$graphLookup无法覆盖双向关联场景,需要调整关联字段的匹配逻辑,或针对树形结构的构建做递归处理。
样本数据
// relationships_collection 样本数据 [ { "_id": ObjectId("60d21b4667d0d8992e610c85"), "name": "总分类", "contains": [ObjectId("60d21b4667d0d8992e610c86"), ObjectId("60d21b4667d0d8992e610c87")], "partOf": [] }, { "_id": ObjectId("60d21b4667d0d8992e610c86"), "name": "子分类A", "contains": [ObjectId("60d21b4667d0d8992e610c88")], "partOf": [ObjectId("60d21b4667d0d8992e610c85")] }, { "_id": ObjectId("60d21b4667d0d8992e610c87"), "name": "子分类B", "contains": [], "partOf": [ObjectId("60d21b4667d0d8992e610c85")] }, { "_id": ObjectId("60d21b4667d0d8992e610c88"), "name": "孙分类A1", "contains": [], "partOf": [ObjectId("60d21b4667d0d8992e610c86")] } ]
目标输出示例(树形层级)
{ "_id": ObjectId("60d21b4667d0d8992e610c85"), "name": "总分类", "contains": [ObjectId("60d21b4667d0d8992e610c86"), ObjectId("60d21b4667d0d8992e610c87")], "partOf": [], "children": [ { "_id": ObjectId("60d21b4667d0d8992e610c86"), "name": "子分类A", "contains": [ObjectId("60d21b4667d0d8992e610c88")], "partOf": [ObjectId("60d21b4667d0d8992e610c85")], "children": [ { "_id": ObjectId("60d21b4667d0d8992e610c88"), "name": "孙分类A1", "contains": [], "partOf": [ObjectId("60d21b4667d0d8992e610c86")], "children": [] } ] }, { "_id": ObjectId("60d21b4667d0d8992e610c87"), "name": "子分类B", "contains": [], "partOf": [ObjectId("60d21b4667d0d8992e610c85")], "children": [] } ] }
调整后的聚合查询方案
方案1:向下构建树形层级(从根节点遍历子节点)
适用于从父节点出发,获取所有子节点的嵌套层级:
db.relationships_collection.aggregate([ // 匹配根节点(这里选择partOf为空的总分类,可根据需求修改匹配条件) { $match: { partOf: { $size: 0 } } }, // 用$graphLookup获取所有子节点及层级 { $graphLookup: { from: "relationships_collection", startWith: "$contains", // 从当前节点的contains数组开始遍历子节点 connectFromField: "contains", // 子节点继续用自己的contains数组遍历下一级 connectToField: "_id", // 匹配目标文档的_id as: "children", // 存储结果的字段名 maxDepth: 10, // 最大遍历层级,按需设置 depthField: "level" // 可选:添加层级深度字段,方便调试 } }, // 将扁平的子节点数组转换为嵌套树形结构 { $addFields: { children: { $function: { body: function(children) { // 用Map存储所有节点,方便快速查找 const nodeMap = new Map(); children.forEach(node => { nodeMap.set(node._id.toString(), { ...node, children: [] }); }); // 遍历节点,根据partOf关联父节点,构建嵌套关系 children.forEach(node => { node.partOf.forEach(parentId => { const parentNode = nodeMap.get(parentId.toString()); if (parentNode) { parentNode.children.push(nodeMap.get(node._id.toString())); } }); }); // 返回根节点的直接子节点 return Array.from(nodeMap.values()).filter(node => node.partOf.some(id => id.toString() === "60d21b4667d0d8992e610c85") ); }, args: ["$children"], lang: "js" } } } } ])
方案2:双向遍历(同时获取父级和子级节点)
如果需要获取某个节点的所有关联层级(包括上级父节点和下级子节点),可使用以下查询:
db.relationships_collection.aggregate([ // 匹配目标节点 { $match: { _id: ObjectId("60d21b4667d0d8992e610c86") } }, { $graphLookup: { from: "relationships_collection", startWith: { $concatArrays: ["$contains", "$partOf"] }, // 同时从contains和partOf开始遍历 connectFromField: { $concatArrays: ["$contains", "$partOf"] }, // 每个节点继续双向遍历 connectToField: "_id", as: "allRelatedNodes", depthField: "depth" // depth为正表示子节点,负表示父节点(可自定义逻辑) } } ])
常见问题修正
你之前的查询可能存在以下问题:
- 仅设置了单方向关联(比如只匹配
partOf而未处理contains),导致层级不完整 - 未对
$graphLookup返回的扁平数组做嵌套处理,无法得到树形结构 - 未限制遍历深度,可能导致性能问题或无限递归
内容的提问来源于stack exchange,提问作者Prabhjot
相关产品推荐
相关产品推荐

