使用MongoDB $graphLookup查询telegramId=200的子节点失败求助
问题描述
我有一个User的Mongoose模型,定义如下:
const schema = new mongoose.Schema( { telegramId_parent: { type: Number, default: null, }, telegramId: { type: Number, required: true, unique: true, }, telegramName: { type: String, }, } );
数据库中的数据为:
[ { _id: new ObjectId('63e8b3951e2a'), telegramId: 111, telegramName: 'ParentTest', telegramId_parent: null, }, { _id: new ObjectId('63e8fa887473'), telegramId: 100, telegramName: 'child1', telegramId_parent: 200, }, { _id: new ObjectId('45a2ec90e'), telegramId: 101, telegramName: 'child2', telegramId_parent: 200, }, { _id: new ObjectId('63c92e'), telegramId: 102, telegramName: 'child1-1', telegramId_parent: 100, }, { _id: new ObjectId('63e96'), telegramId: 200, telegramName: 'Name1', telegramId_parent: 111, }, ];
我想要获取telegramId=200、深度为3的所有子节点,预期结果如下:
{ _id: new ObjectId('63e96'), telegramId: 200, telegramName: 'Name1', telegramId_parent: 111, childs:[ { _id: new ObjectId('63e8fa887473'), telegramId: 100, telegramName: 'child1', telegramId_parent: 200, }, { _id: new ObjectId('45a2ec90e'), telegramId: 101, telegramName: 'child2', telegramId_parent: 200, }, { _id: new ObjectId('63c92e'), telegramId: 102, telegramName: 'child1-1', telegramId_parent: 100, }, ] }
我的查询语句为:
User.aggregate([ { $match: { telegramId: 200 } }, { $graphLookup: { from: 'Users', startWith: '$telegramId', connectFromField: 'telegramId', connectToField: 'telegramId_parent', as: 'child', maxDepth: 3, }, }, ]);
但实际得到的结果中childs数组为空:
{ _id: new ObjectId('63e96'), telegramId: 200, telegramName: 'Name1', telegramId_parent: 111, childs:[] }
请问为什么查询不到预期的子节点?如何获取带层级的结果?
问题原因与解决方案
一、查询不到子节点的核心原因
你的$graphLookup参数存在两个关键问题:
- 集合名称不匹配:Mongoose会自动将模型名转为小写复数形式,比如
User模型对应的实际集合名是users,但你写的是from: 'Users'(大写开头),导致无法找到目标集合,返回空数组。 - 字段命名不一致:查询中
as字段定义为'child',但你预期结果的字段名是childs,后续处理时的命名差异也会导致显示异常。
二、修正查询获取平级子节点(匹配预期结果)
将from改为正确的集合名,并统一字段命名:
User.aggregate([ { $match: { telegramId: 200 } }, { $graphLookup: { from: 'users', // 对应Mongoose自动生成的小写复数集合名 startWith: '$telegramId', connectFromField: 'telegramId', // 父节点的telegramId作为匹配基准 connectToField: 'telegramId_parent', // 子节点的telegramId_parent需等于基准值 as: 'childs', // 和预期结果的字段名保持一致 maxDepth: 3, }, }, ]);
执行后就能得到所有深度≤3的子节点,平级放在childs数组中,完全匹配你的预期结果。
三、获取带层级的嵌套结果
如果需要子节点按层级嵌套(比如child1下包含child1-1),可以通过两种方式实现:
方式1:聚合查询内构建层级
利用depthField记录节点深度,再通过$map和$filter重组嵌套结构:
User.aggregate([ { $match: { telegramId: 200 } }, { $graphLookup: { from: 'users', startWith: '$telegramId', connectFromField: 'telegramId', connectToField: 'telegramId_parent', as: 'allChilds', maxDepth: 3, depthField: 'depth' // 添加深度字段用于分组 }, }, { $addFields: { tree: { $mergeObjects: [ '$$ROOT', { childs: { $filter: { input: '$allChilds', cond: { $eq: ['$$this.depth', 1] } // 筛选第一层子节点 } } } ] } } }, { $addFields: { 'tree.childs': { $map: { input: '$tree.childs', as: 'child', in: { $mergeObjects: [ '$$child', { childs: { $filter: { input: '$allChilds', cond: { $eq: ['$$this.telegramId_parent', '$$child.telegramId'] } } } } ] } } } } }, { $replaceRoot: { newRoot: '$tree' } }, { $project: { allChilds: 0 } } // 移除临时平级字段 ]);
返回结果会是嵌套层级结构:
{ _id: new ObjectId('63e96'), telegramId: 200, telegramName: 'Name1', telegramId_parent: 111, childs: [ { _id: new ObjectId('63e8fa887473'), telegramId: 100, telegramName: 'child1', telegramId_parent: 200, depth: 1, childs: [ { _id: new ObjectId('63c92e'), telegramId: 102, telegramName: 'child1-1', telegramId_parent: 100, depth: 2 } ] }, { _id: new ObjectId('45a2ec90e'), telegramId: 101, telegramName: 'child2', telegramId_parent: 200, depth: 1, childs: [] } ] }
方式2:代码层面递归构建层级
如果聚合查询逻辑过于复杂,可以先获取平级子节点,再用递归函数构建嵌套结构:
// 第一步:查询父节点及所有平级子节点 const [parent] = await User.aggregate([ { $match: { telegramId: 200 } }, { $graphLookup: { from: 'users', startWith: '$telegramId', connectFromField: 'telegramId', connectToField: 'telegramId_parent', as: 'allChilds', maxDepth: 3, }, }, ]); // 第二步:递归构建嵌套树结构 function buildTree(nodes, parentId) { return nodes .filter(node => node.telegramId_parent === parentId) .map(node => ({ ...node, childs: buildTree(nodes, node.telegramId) })); } parent.childs = buildTree(parent.allChilds, parent.telegramId); delete parent.allChilds; console.log(parent);
这种方式逻辑更直观,适合层级关系复杂的场景。
内容的提问来源于stack exchange,提问作者ogggy
相关产品推荐
相关产品推荐

