如何在MongoDB树形结构中单独获取所有评论文档?
问题背景
我在MongoDB中有如下结构的评论文档:
{ "_id": { "$oid": "61acd4474bbaa88d12c38199" }, "textContent": "这条评论有2条回复", "replies": [ { "$oid": "61acd57fe06fed9e70462139" }, { "$oid": "61acd57fe06fed9e7046213a" }, ], "__v": 0 }, { "_id": { "$oid": "61acd57fe06fed9e70462139" }, "textContent": "这条评论没有回复", "replies": [], "__v": 0 }, { "_id": { "$oid": "61acd57fe06fed9e7046213a" }, "textContent": "这条评论没有回复", "replies": [], "__v": 0 }
所有评论都是同类型文档,replies数组存储其他评论的ID,构成树形结构但每个文档独立存储。
我希望从服务器返回数据时,评论仍保持独立文档的形式,仅保留对其他评论的ID引用(和存储结构一致)。现在需要单次查询获取所有相关评论,且保持文档独立,注意嵌套层级可能比示例更深,而且预先不知道顶级评论的ID。
我知道Mongoose的populate可以查询嵌套文档,但它会返回嵌套的大对象,而不是独立文档:
this.populate({ path: 'replies' })
它的输出是:
{ _id: "61acd4474bbaa88d12c38199", textContent: "这条评论有2条回复", replies: [ { _id: "61acd57fe06fed9e70462139", textContent: "这条评论没有回复", replies: [] }, { _id: "61acd57fe06fed9e7046213a", textContent: "这条评论没有回复", replies: [] } ] }
我的期望输出是:
[ { _id: "61acd4474bbaa88d12c38199", textContent: "这条评论有2条回复", replies: [ "61acd57fe06fed9e70462139", "61acd57fe06fed9e7046213a" ] }, { _id: "61acd57fe06fed9e70462139", textContent: "这条评论没有回复", replies: [ ] }, { _id: "61acd57fe06fed9e7046213a", textContent: "这条评论没有回复", replies: [ ] } ]
解决方案
方法1:使用MongoDB聚合查询(推荐)
利用$graphLookup可以一次性递归获取所有层级的关联评论,同时保持文档独立:
const allComments = await Comment.aggregate([ // 递归查询所有关联的评论ID,覆盖所有嵌套层级 { $graphLookup: { from: "comments", // 替换为你的评论集合名称 startWith: "$_id", connectFromField: "replies", connectToField: "_id", as: "relatedIds" } }, // 筛选出顶级评论(未被任何其他评论引用的) { $match: { $expr: { $not: { $in: ["$_id", "$relatedIds.replies"] } } } }, // 收集所有关联ID并去重 { $group: { _id: null, allCommentIds: { $addToSet: "$relatedIds._id" } } }, // 展开ID数组 { $unwind: "$allCommentIds" }, { $unwind: "$allCommentIds" }, // 根据ID查询对应评论文档 { $lookup: { from: "comments", localField: "allCommentIds", foreignField: "_id", as: "comment" } }, { $unwind: "$comment" }, // 将replies中的ObjectId转为字符串,移除__v字段 { $addFields: { "comment.replies": { $map: { input: "$comment.replies", as: "replyId", in: { $toString: "$$replyId" } } } } }, { $project: { "comment.__v": 0 } }, // 整理为期望的评论数组格式 { $replaceRoot: { newRoot: "$comment" } } ]);
如果不需要筛选顶级评论,直接查询所有评论并格式化:
const allComments = await Comment.aggregate([ { $match: {} }, { $addFields: { replies: { $map: { input: "$replies", as: "replyId", in: { $toString: "$$replyId" } } } } }, { $project: { __v: 0 } } ]);
方法2:Mongoose查询+简单后处理
如果聚合逻辑复杂,可先查询所有评论,再做格式转换:
// 查询所有评论,排除__v字段 const rawComments = await Comment.find({}, { __v: 0 }); // 转换replies中的ObjectId为字符串 const formattedComments = rawComments.map(comment => ({ ...comment.toObject(), replies: comment.replies.map(id => id.toString()) }));
若仅需获取关联评论(而非全部),可先递归收集所有关联ID再批量查询:
// 找出所有顶级评论 const topComments = await Comment.find({ _id: { $nin: await Comment.distinct("replies") } }); // 递归收集所有关联评论ID const collectAllIds = async (ids) => { const comments = await Comment.find({ _id: { $in: ids } }); const childIds = comments.flatMap(c => c.replies); if (!childIds.length) return ids; const nestedIds = await collectAllIds(childIds); return [...new Set([...ids, ...nestedIds])]; }; const allIds = await collectAllIds(topComments.map(c => c._id)); // 批量查询并格式化 const formattedComments = (await Comment.find({ _id: { $in: allIds } }, { __v: 0 })).map(comment => ({ ...comment.toObject(), replies: comment.replies.map(id => id.toString()) }));
关键说明
- 若输出允许保留
ObjectId类型,可去掉所有$map或map中的字符串转换逻辑。 $graphLookup支持MongoDB 3.4+,能高效处理深层嵌套的树形结构查询。- 如需限定查询范围(比如某篇文章的评论),只需在
$match或find中添加对应条件(如articleId: 目标ID)。
内容的提问来源于stack exchange,提问作者Candleout
相关产品推荐
相关产品推荐

