MongoDB多集合$lookup聚合在CosmosDB(Mongo API)与MongoDB中的差异及实现方案
我清楚非关系型数据库中这种数据建模存在问题,但这是继承而来的架构,目前需要临时解决方案。我们有三个集合collectionA、collectionB、collectionC,关系为collectionA与collectionB是一对多,collectionB与collectionC是一对多,所有子文档均存储父文档的引用。
现在需要高效获取这三个集合的聚合数据,但我们的方案在**CosmosDB(启用Mongo API)和MongoDB测试容器(mongo:4.2.0)**中得到了不同结果。
我们尝试用包含两个$lookup阶段的聚合管道,在CosmosDB中得到了期望结果:
[ { $lookup: { from: "collectionB", localField: "_id", foreignField: "CollectionBId", as: "collectionBList", }, }, { $lookup: { from: "collectionC", localField: "collectionBList._id", foreignField: "collectionCId", as: "collectionBList.collectionCList", }, }, ]
结果示例:
{ _id: ..., collectionBList: [ { ... collectionCList: [...] } ] }
但在mongo:4.2.0中执行相同管道,结果不符合预期:
{ _id: ..., collectionBList: { collectionCList: [...] } }
注意:collectionBList从数组变成了对象
关于差异的理解
你的理解是正确的。MongoDB官方文档明确说明,$lookup的as字段如果已存在于输入文档中,会直接覆盖现有字段。在原生MongoDB 4.2中,第二个$lookup指定as: "collectionBList.collectionCList"时,会把原本是数组的collectionBList覆盖成一个包含collectionCList字段的对象,这是符合原生MongoDB行为的。
而CosmosDB的Mongo API属于兼容实现,并非原生MongoDB,存在这种行为差异是已知的兼容问题——它对嵌套字段的$lookup处理逻辑和原生MongoDB不一致,允许直接往数组元素中嵌入关联数据,而不是覆盖整个数组字段。
核心问题:原生MongoDB中实现嵌套关联聚合的方案
要在原生MongoDB中实现把collectionC数据嵌套到对应collectionB元素中的效果,有两种可行方案:
方案1:通过$unwind+$group实现(兼容低版本MongoDB)
通过拆分数组、关联数据后重新分组,步骤如下:
[ // 第一步:关联collectionB,得到collectionBList数组 { $lookup: { from: "collectionB", localField: "_id", foreignField: "CollectionBId", as: "collectionBList" } }, // 第二步:拆分collectionBList数组,每个元素单独成文档 { $unwind: "$collectionBList" }, // 第三步:关联collectionC,将对应数据放到当前collectionB元素的collectionCList字段 { $lookup: { from: "collectionC", localField: "collectionBList._id", foreignField: "collectionCId", as: "collectionBList.collectionCList" } }, // 第四步:按collectionA的_id重新分组,重组collectionBList数组,同时保留原字段 { $group: { _id: "$_id", // 手动指定需要保留的collectionA其他字段,示例: name: { $first: "$name" }, createTime: { $first: "$createTime" }, collectionBList: { $push: "$collectionBList" } } } ]
说明:如果collectionA有其他需要保留的字段,需在$group阶段用$first指定对应字段
方案2:使用带pipeline的$lookup(推荐,MongoDB 3.6+支持)
在第一个$lookup内部嵌套关联逻辑,无需拆分重组数组,更高效:
[ { $lookup: { from: "collectionB", localField: "_id", foreignField: "CollectionBId", as: "collectionBList", // 嵌套关联collectionC pipeline: [ { $lookup: { from: "collectionC", localField: "_id", foreignField: "collectionCId", as: "collectionCList" } } ] } } ]
内容的提问来源于stack exchange,提问作者dapperwaterbuffalo

