首页 > 解决方案 > 在内部嵌套数组中分组 $lookup 结果,并在 MongoDB 中保持外部嵌套数组及其内容完整

问题描述

我有一个包含三层数组嵌套的集合,如下所示

_id: ObjectID('abc'),
sections: [
  {
    sectionId: "sec0",
    sectionName: "ABC",
    contents: [
      {
        contentId: 0,
        tasks: [
           {
             taskId: ObjectID('task1')
           }
           //May contain 1-100 tasks
        ],
        contentDescription: "Content is etc",
      }
    ]
  }
]

Sections 是一个对象数组,其中包含一个对象,每个对象都有一个 sectionId,contents 数组是一个对象数组,由 contentId、contentDescription 和嵌套的任务数组组成,其中包含一个包含 taskId 的对象。我正在应用 $lookup 运算符,以便将嵌套任务数组与任务集合连接起来,但我在文档重复方面遇到了问题,如下所示。

_id: ObjectID('abc'),
sections: [
  {
    sectionId: "sec0",
    sectionName: "ABC",
    contents: [
      {
        contentId: 0,
        tasks: [
           {
             //Task Document of ID 1
           }
        ],
        contentDescription: "Content is etc",
      }
    ]
  }
]
_id: ObjectID('abc'),
sections: [
  {
    sectionId: "sec0",
    sectionName: "ABC",
    contents: [
      {
        contentId: 0,
        tasks: [
           {
             //Task Document of ID 2
           }
        ],
        contentDescription: "Content is etc",
      }
    ]
  }
]

所需的输出如下

_id: ObjectID('abc'),
sections: [
  {
    sectionId: "sec0",
    sectionName: "ABC",
    contents: [
      {
        contentId: 0,
        tasks: [
           {
             //Task Document of ID 1
           },
           {
             //Task Document of ID 2
           },
           {
             //Task Document of ID 3
           }
        ],
        contentDescription: "Content is etc",
      }
    ]
  }
]

在集合中,sections 数组可能包含多个 section 对象,这些对象可能包含多个内容等等。有问题的架构是临时的,因为我们公司目前正在从现有数据库迁移到 MongoDB,因此架构重构是不可能的,我需要使用来自不同数据库的现有架构设计。

我尝试了以下方式

const contents= await sections.aggregate([
    {
      $match: { _id: id},
    },
    { $unwind: '$sections' },
    {
      $unwind: {
        path: '$sections.contents',
        preserveNullAndEmptyArrays: true,
      },
    },
    {
      $unwind: {
        path: '$sections.contents.tasks',
        preserveNullAndEmptyArrays: true,
      },
    },
    {
      $lookup: {
        from: 'tasks',
        let: { task_id: '$sections.contents.tasks.taskId' },
        pipeline: [
          { $match: { $expr: { $eq: ['$_id', '$$task_id'] } } },
        ],
        as: 'sections.contents.tasks',
      },
    },
    {
      $addFields: {
        'sections.contents.tasks': {
          $arrayElemAt: ['$sections.contents.tasks', 0],
        },
      },
    },
    {
      $group: {
        _id: '$_id',
        exam: { $push: '$sections.contents.tasks' },
      },
    },
  ]);

而且我也无法使用 $group 聚合运算符

$group: {
        _id: '$_id',
        sections: {
           sectionId : { $first: '$sectionId' },
           sectionName: { $first: '$sectionName' },
           contents: {
              contentId: { $first: '$contentId' },
              task: { $push: $sections.contents.tasks }
           }
         },
        },

任何帮助或指示将不胜感激,我也在 SO 上进行了搜索,发现了这个但无法理解以下部分

 {"$group":{
   "_id":{"_id":"$_id","mission_id":"$missions._id"},
   "agent":{"$first":"$agent"},
   "title":{"$first":"$missions.title"},
   "clients":{"$push":"$missions.clients"}
 }},
 {"$group":{
   "_id":"$_id._id",
   "missions":{
     "$push":{
       "_id":"$_id.mission_id",
       "title":"$title",
       "clients":"$clients"
      }
    }
 }}

标签: node.jsmongodbaggregation-framework

解决方案


所以你非常接近最终的解决方案,一个很好的“规则”要记住,如果你放松x时间,你需要分组x以正确恢复原始结构,如下所示:

db.collection.aggregate([
  {
    $match: {
      _id: id
    },
  },
  {
    $unwind: "$sections"
  },
  {
    $unwind: {
      path: "$sections.contents",
      preserveNullAndEmptyArrays: true,
    },
  },
  {
    $unwind: {
      path: "$sections.contents.tasks",
      preserveNullAndEmptyArrays: true,
    },
  },
  {
    $lookup: {
      from: "tasks",
      let: {
        task_id: "$sections.contents.tasks.taskId"
      },
      pipeline: [
        {
          $match: {
            $expr: {
              $eq: [
                "$_id",
                "$$task_id"
              ]
            }
          }
        },
        
      ],
      as: "sections.contents.tasks",
    },
  },
  {
    $addFields: {
      "sections.contents.tasks": {
        $arrayElemAt: [
          "$sections.contents.tasks",
          0
        ],
      },
    },
  },
  {
    $group: {
      _id: {
        contentId: "$sections.contents.contentId",
        sectionId: "$sections.sectionId",
        sectionName: "$sections.sectionName",
        originalId: "$_id"
      },
      tasks: {
        $push: "$sections.contents.tasks"
      },
      contentDescription: {
        $first: "$sections.contents.contentDescription"
      },
    }
  },
  {
    $group: {
      _id: {
        sectionId: "$_id.sectionId",
        sectionName: "$_id.sectionName",
        originalId: "$_id.originalId"
      },
      contents: {
        $push: {
          contentId: "$_id.contentId",
          tasks: "$tasks",
          contentDescription: "$contentDescription"
        }
      }
    }
  },
  {
    $group: {
      _id: "$_id.originalId",
      sections: {
        $push: {
          sectionId: "$_id.sectionId",
          sectionName: "$_id.sectionName",
          contents: "$contents"
        }
      }
    }
  }
])

蒙戈游乐场

但是,您的管道可以变得更清洁,因为它有 1 个冗余$unwind阶段,也添加了一个冗余$group阶段。我不会在这里发布整个固定管道,因为它已经是一个很长的帖子,但请随时在这里查看:Mongo Playground fixed


推荐阅读