MongoDB聚合查询:如何关联集合并过滤嵌套数组元素?
MongoDB关联查询:过滤学生课程列表并保留活跃项
集合结构
students集合文档
{ _id: ObjectId('66880dd88517a33fd8141e08'), name: 'Mark', courses: [ {course_id: ObjectId('6684e9c8122ef4d77a9dfd1a'), active: true}, {course_id: ObjectId('6684e9c8122ef4d77a9dfd1b'), active: false} ] } { _id: ObjectId('668626f11b64d2e86632e7cb'), name: 'John', courses: [ {course_id: ObjectId('6684e9c8122ef4d77a9dfd1a'), active: false}, {course_id: ObjectId('6684e9c8122ef4d77a9dfd1b'), active: true} ] } { _id: ObjectId('6684e3b628fcee8f24ead736'), name: 'Doe', courses: [ {course_id: ObjectId('6684e9c8122ef4d77a9dfd1a'), active: true}, {course_id: ObjectId('6684e9c8122ef4d77a9dfd1b'), active: true} ] }
courses集合文档
{ _id: ObjectId('6684e9c8122ef4d77a9dfd1a'), note: 'Course on mongodb.' } { _id: ObjectId('6684e9c8122ef4d77a9dfd1b'), note: 'Advanced mongodb course.' }
查询需求
查询courses集合中指定_id的文档,需附带students数组:
- 数组仅包含**选修该课程且课程状态为
active: true**的学生 - 每个学生的
courses数组仅保留匹配指定课程_id且active: true的项
示例:查询_id为6684e9c8122ef4d77a9dfd1b的课程,期望输出:
{ _id: ObjectId('6684e9c8122ef4d77a9dfd1b'), note: 'Advanced mongodb course.', students: [ { _id: ObjectId('668626f11b64d2e86632e7cb'), name: 'John', courses: [ {course_id: ObjectId('6684e9c8122ef4d77a9dfd1b'), active: true} ] }, { _id: ObjectId('6684e3b628fcee8f24ead736'), name: 'Doe', courses: [ {course_id: ObjectId('6684e9c8122ef4d77a9dfd1b'), active: true} ] } ] }
现有问题代码
当前使用的聚合查询未过滤学生的课程列表,返回结果不符合预期:
app.get('/:id', async function (req, res) { try { const batch = await Course.aggregate([ {"$match": {"_id": req.params.id}}, {"$lookup": { "from": "students", "localField": "_id", "foreignField": "courses.course_id", "as": "students" } } ]) res.send(batch) } catch (error) { res.json({ error: error }) } })
修改方案
需要使用带内部管道的$lookup,同时处理学生的课程过滤,并且注意将字符串ID转换为MongoDB的ObjectId类型:
const mongoose = require('mongoose'); app.get('/:id', async function (req, res) { try { // 转换请求参数为ObjectId,匹配MongoDB存储类型 const courseId = mongoose.Types.ObjectId(req.params.id); const result = await Course.aggregate([ { "$match": { "_id": courseId } }, { "$lookup": { "from": "students", "let": { "targetCourseId": "$_id" }, "pipeline": [ // 筛选出选修该课程且active为true的学生 { "$match": { "$expr": { "$anyElementTrue": [ { "$map": { "input": "$courses", "as": "c", "in": { "$and": [ { "$eq": ["$$c.course_id", "$$targetCourseId"] }, { "$eq": ["$$c.active", true] } ]} }} ] } } }, // 过滤学生的courses数组,仅保留目标课程的活跃项 { "$addFields": { "courses": { "$filter": { "input": "$courses", "as": "c", "cond": { "$and": [ { "$eq": ["$$c.course_id", "$$targetCourseId"] }, { "$eq": ["$$c.active", true] } ]} } } } } ], "as": "students" } } ]); // 聚合返回数组,直接返回第一个结果或空对象 res.send(result[0] || {}); } catch (error) { res.json({ error: error.message }); } })
关键修改说明
- ID类型转换:将
req.params.id转换为mongoose.Types.ObjectId,确保与MongoDB存储的_id类型匹配,避免匹配失败。 - 带内部管道的$lookup:通过
let传递当前课程ID到内部管道,实现更精细的关联逻辑:- 内部
$match阶段:用$expr结合$anyElementTrue和$map,筛选出拥有目标活跃课程的学生。 - 内部
$addFields阶段:用$filter过滤学生的courses数组,仅保留符合条件的课程项。
- 内部
- 结果处理:聚合返回的是数组,直接返回第一个元素(或空对象),符合单条课程查询的场景。
内容的提问来源于stack exchange,提问作者Tahsin Al Mahi
相关产品推荐
相关产品推荐

