您在这里缺少的是$lookup 在其参数中as 指定的输出字段中生成一个“数组”。这是 MongoDB “关系”的一般概念,因为文档之间的“关系”表示为文档本身“内部”的“子属性”,对于许多人来说,要么是单数,要么是“数组”。
由于 MongoDB 是“无模式”的,$lookup 的一般假设是您的意思是“很多”,因此结果“总是”是一个数组。因此,寻找“与 SQL 中相同的结果”然后您需要在 $lookup 之后的 $unwind 那个数组。无论是“一”还是“多”都无关紧要,因为它仍然“始终”是一个数组:
db.getCollection.('tb1').aggregate([
// Filter conditions from the source collection
{ "$match": { "status": { "$ne": "closed" } }},
// Do the first join
{ "$lookup": {
"from": "tb2",
"localField": "id",
"foreignField": "profileId",
"as": "tb2"
}},
// $unwind the array to denormalize
{ "$unwind": "$tb2" },
// Then match on the condtion for tb2
{ "$match": { "tb2.profile_type": "agent" } },
// join the second additional collection
{ "$lookup": {
"from": "tb3",
"localField": "tb2.id",
"foreignField": "id",
"as": "tb3"
}},
// $unwind again to de-normalize
{ "$unwind": "$tb3" },
// Now filter the condition on tb3
{ "$match": { "tb3.status": 0 } },
// Project only wanted fields. In this case, exclude "tb2"
{ "$project": { "tb2": 0 } }
])
这里您需要注意翻译中缺少的其他内容:
顺序很“重要”
聚合管道比 SQL 更“简洁地表达”。实际上,最好将它们视为“一系列步骤”,应用于数据源以整理和转换数据。最好的模拟是“管道”命令行指令,例如:
ps -ef | grep mongod | grep -v grep | awk '{ print $1 }'
“管道”| 可以被视为 MongoDB 聚合“管道”中的“管道阶段”。
因此,我们希望 $match 将“源”集合中的内容过滤为我们的第一个操作。这通常是一种很好的做法,因为它会从进一步的条件中删除任何不符合所需条件的文件。就像我们的“命令行管道”示例中发生的那样,我们将“输入”然后“管道”到grep 以“删除”或“过滤”。
路径很重要
您接下来要做的就是通过$lookup“加入”。结果是来自"from" 集合参数的项目的“数组”,与提供的字段匹配,以在"as"“字段路径”中作为“数组”输出。
此处选择的命名很重要,因为现在源集合中的“文档”认为“连接”中的所有项目现在都存在于该给定路径中。为方便起见,我使用与新“路径”的“连接”相同的“集合”名称。
所以从第一个“加入”开始,输出到"tb2",它将保存该集合的所有结果。对于 MongoDB 实际处理查询的方式,下面的$unwind 和$match 序列还有一个重要的事情需要注意。
某些序列“真的”很重要
因为它“看起来”有“三个”管道阶段,分别是$lookup,然后是$unwind,然后是$match。但实际上,MongoDB 确实做了其他事情,这在添加到 .aggregate() 命令的 { "explain": true } 的输出中得到了证明:
{
"$lookup" : {
"from" : "tb2",
"as" : "tb2",
"localField" : "id",
"foreignField" : "profileId",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"profile_type" : {
"$eq" : "agent"
}
}
}
},
{
"$lookup" : {
"from" : "tb3",
"as" : "tb3",
"localField" : "tb2.id",
"foreignField" : "id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"status" : {
"$eq" : 0.0
}
}
}
},
因此,除了应用“序列”的第一点之外,您需要将$match 语句放在需要它们的地方并做“最好的”,这实际上对于“连接”的概念变得“非常重要” .这里要注意的是,我们的$lookup、$unwind 和$match 序列实际上被 MongoDB 处理为只是 $lookup 阶段,而其他操作“汇总”到一个管道阶段每个。
这是与“过滤”$lookup 获得的结果的其他方式的重要区别。因为在这种情况下,来自$match 的“join”的实际“查询”条件是在集合上执行 join“之前”结果返回给父级。
结合上图所示的$unwind(翻译成unwinding)是MongoDB实际处理“连接”可能导致在源文档中产生一个内容数组的可能性,从而导致它超过 16MB BSON 限制。这只会发生在被连接的结果非常大的情况下,但同样的优势在于实际应用“过滤器”的地方,即在返回结果“之前”的目标集合上。
正是这种处理与 SQL JOIN 的相同行为最“相关”。因此,它也是从 $lookup 获取结果的最有效方法,其中除了简单的“外来”键值的“本地”之外,还有其他条件适用于 JOIN。
另请注意,另一个行为变化来自$lookup 执行的本质上的左连接,其中无论“目标”集合中是否存在匹配文档,都将始终保留“源”文档。相反,$unwind 通过在$match 中的附加条件“丢弃”来自“源”的任何结果,这些结果与“目标”没有任何匹配。
事实上,由于包含隐含的preserveNullAndEmptyArrays: false,它们甚至被预先丢弃,并且会丢弃两个集合之间“本地”和“外来”键甚至不匹配的任何内容。这对于这种特定类型的查询来说是一件好事,因为“连接”旨在对这些值进行“相等”。
结束
如前所述,MongoDB 对待“关系”的方式通常与使用“关系数据库”或 RDBMS 的方式大不相同。 “关系”的一般概念实际上是将数据“嵌入”为单个属性或数组。
你可能真的想要这样的输出,这也是为什么没有$unwind 序列,$lookup 的输出实际上是一个“数组”的部分原因。然而,在这种情况下使用$unwind 实际上是最有效的做法,并且保证“加入”数据实际上不会因为“加入”而导致上述 BSON 限制被超过。
如果你真的想要输出数组,那么最好的办法是使用$group 管道阶段,并且可能作为多个阶段来“规范化”和“撤消”$unwind 的结果
{ "$group": {
"_id": "$_id",
"tb1_field": { "$first": "$tb1_field" },
"tb1_another": { "$first": "$tb1_another" },
"tb3": { "$push": "$tb3" }
}}
实际上,在这种情况下,您可以使用 $first 按属性名称列出 "tb1" 所需的所有字段,以仅保留“第一次”出现(基本上由 "tb2" 和 "tb3" 的结果重复) unwound ) 然后将$push 中的“细节”从"tb3" 放入一个“数组”中,以表示与"tb1" 的关系。
但是给定的聚合管道的一般形式是如何从原始 SQL 获得结果的精确表示,它是作为“连接”结果的“非规范化”输出。在此之后是否要再次“标准化”结果取决于您。