You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 });
    }
})

关键修改说明

  1. ID类型转换:将req.params.id转换为mongoose.Types.ObjectId,确保与MongoDB存储的_id类型匹配,避免匹配失败。
  2. 带内部管道的$lookup:通过let传递当前课程ID到内部管道,实现更精细的关联逻辑:
    • 内部$match阶段:用$expr结合$anyElementTrue和$map,筛选出拥有目标活跃课程的学生。
    • 内部$addFields阶段:用$filter过滤学生的courses数组,仅保留符合条件的课程项。
  3. 结果处理:聚合返回的是数组,直接返回第一个元素(或空对象),符合单条课程查询的场景。

内容的提问来源于stack exchange,提问作者Tahsin Al Mahi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 10:12:10