订购文档示例:
{
_id: ObjectId("..."),
products: [
ObjectId("..<Car ObjectId>.."),
ObjectId("..<Bike ObjectId>..")
]
}
不正常的查询:
db.orders.aggregate([
{
$lookup:
{
from: "products",
localField: "products",
foreignField: "_id",
as: "productObjects"
}
}
])
所需结果
{
_id: ObjectId("..."),
products: [
ObjectId("..<Car ObjectId>.."),
ObjectId("..<Bike ObjectId>..")
],
productObjects: [
{<Car Object>},
{<Bike Object>}
],
}
#1 楼
2017更新$ lookup现在可以直接使用数组作为本地字段。不再需要
$unwind
。旧答案
$lookup
聚合管道阶段不能直接用于数组。该设计的主要目的是将“左连接”作为可能的相关数据上的“一对多”连接类型(或实际上是“查找”)。但是该值应为单数而不是数组。因此,在执行
$lookup
操作之前,必须首先对内容进行“去规范化”才能使其正常工作。这意味着使用$unwind
: db.orders.aggregate([
// Unwind the source
{ "$unwind": "$products" },
// Do the lookup matching
{ "$lookup": {
"from": "products",
"localField": "products",
"foreignField": "_id",
"as": "productObjects"
}},
// Unwind the result arrays ( likely one or none )
{ "$unwind": "$productObjects" },
// Group back to arrays
{ "$group": {
"_id": "$_id",
"products": { "$push": "$products" },
"productObjects": { "$push": "$productObjects" }
}}
])
在
$lookup
匹配每个数组成员之后,结果是数组本身,因此请再次$unwind
和$group
到$push
新数组以获取最终结果。请注意,未找到的任何“左连接”匹配都将为上的“ productObjects”创建一个空数组给定产品,从而在调用第二个
$unwind
时否定“ product”元素的文档。虽然直接应用到数组会很好,但这正是当前通过匹配奇异值来工作的方式
由于
$lookup
基本上是一个非常新的东西,因此它现在可以像熟悉猫鼬的人一样使用,它是那里提供的.populate()
方法的“穷人版”。区别在于$lookup
提供了“连接”的“服务器端”处理,而不是客户端上的处理,而$lookup
提供的功能目前缺乏某些“成熟度”(例如,直接在数组上内插查找) 。这实际上是改进服务器SERVER-22881的分配问题,因此,如果运气好的话,它将在下一个版本或之后的一个版本中发布。
作为一种设计原则,您当前的结构既不是好事,也不是坏事,而在创建任何“联接”时只会受到开销的影响。因此,MongoDB从一开始就具有基本的站立原则,如果您可以“容纳”一个集合中的“预加入”数据,那么最好这样做。
.populate()
的一般原则可以说的另一件事是,此处的“ join”的目的是与此处所示的相反。因此,与其将其他文档的“相关ID”保留在“父”文档中,不如将最有效的一般原则是“相关文档”包含对“父”的引用。因此,可以说
$lookup
与“关系设计”是“最有效的”,这与像猫鼬$lookup
这样的客户端联接的执行方式相反。通过在每个“许多”中标识“一个”,则只需提取相关项,而无需先对数组进行遍历。评论
谢谢你的作品!这是否表明我的数据的结构/规范化不正确?
–林俊杰
16年1月23日在20:33
@JasonLin不如“好/不好”那样精明,因此在答案中添加了更多解释。这取决于什么适合您。
–布雷克七世
16年1月23日在20:48
当前的实现有些无意。查找本地字段数组中的所有值是有意义的,按字面意义使用该数组是没有意义的,因此SERVER-22881会跟踪该问题。
– Asya Kamsky
16 Mar 16 '16 at 22:05
@AsyaKamsky有意义。通常,我一直将查询,查询和文档验证视为婴儿期的特征,并且可能会有所改善。因此,欢迎在数组上进行直接扩展,也可以使用“查询”来过滤结果。两者都将与许多人惯用的mongoose .populate()流程更加一致。将问题链接直接添加到答案内容中。
–布雷克七世
16-3-16在22:29
请注意,按照下面的答案,这已经实现,并且$ lookup现在可以直接在数组上工作。
–亚当·里斯(Adam Reis)
19年2月9日在3:43
#2 楼
从MongoDB v3.4(2016年发布)开始,$lookup
聚合管道阶段也可以直接与阵列一起使用。不再需要$unwind
。在SERVER-22881中进行了跟踪。
#3 楼
您还可以使用pipeline
阶段对子文档阵列执行检查。下面是使用
python
的示例(对不起,我是蛇人)。db.products.aggregate([
{ '$lookup': {
'from': 'products',
'let': { 'pid': '$products' },
'pipeline': [
{ '$match': { '$expr': { '$in': ['$_id', '$$pid'] } } }
// Add additional stages here
],
'as':'productObjects'
}
])
这里的要点是匹配
ObjectId
array
(位于_id
字段/ prop local
中的外国products
)中的所有对象。您还可以清理或投影其他外部记录
stage
s,如上面的注释所示。#4 楼
使用$ unwind,您将获得第一个对象而不是对象数组查询:
db.getCollection('vehicles').aggregate([
{
$match: {
status: "AVAILABLE",
vehicleTypeId: {
$in: Array.from(newSet(d.vehicleTypeIds))
}
}
},
{
$lookup: {
from: "servicelocations",
localField: "locationId",
foreignField: "serviceLocationId",
as: "locations"
}
},
{
$unwind: "$locations"
}
]);
结果:
{
"_id" : ObjectId("59c3983a647101ec58ddcf90"),
"vehicleId" : "45680",
"regionId" : 1.0,
"vehicleTypeId" : "10TONBOX",
"locationId" : "100",
"description" : "Isuzu/2003-10 Ton/Box",
"deviceId" : "",
"earliestStart" : 36000.0,
"latestArrival" : 54000.0,
"status" : "AVAILABLE",
"accountId" : 1.0,
"locations" : {
"_id" : ObjectId("59c3afeab7799c90ebb3291f"),
"serviceLocationId" : "100",
"regionId" : 1.0,
"zoneId" : "DXBZONE1",
"description" : "Masafi Park Al Quoz",
"locationPriority" : 1.0,
"accountTypeId" : 0.0,
"locationType" : "DEPOT",
"location" : {
"makani" : "",
"lat" : 25.123091,
"lng" : 55.21082
},
"deliveryDays" : "MTWRFSU",
"timeWindow" : {
"timeWindowTypeId" : "1"
},
"address1" : "",
"address2" : "",
"phone" : "",
"city" : "",
"county" : "",
"state" : "",
"country" : "",
"zipcode" : "",
"imageUrl" : "",
"contact" : {
"name" : "",
"email" : ""
},
"status" : "",
"createdBy" : "",
"updatedBy" : "",
"updateDate" : "",
"accountId" : 1.0,
"serviceTimeTypeId" : "1"
}
}
{
"_id" : ObjectId("59c3983a647101ec58ddcf91"),
"vehicleId" : "81765",
"regionId" : 1.0,
"vehicleTypeId" : "10TONBOX",
"locationId" : "100",
"description" : "Hino/2004-10 Ton/Box",
"deviceId" : "",
"earliestStart" : 36000.0,
"latestArrival" : 54000.0,
"status" : "AVAILABLE",
"accountId" : 1.0,
"locations" : {
"_id" : ObjectId("59c3afeab7799c90ebb3291f"),
"serviceLocationId" : "100",
"regionId" : 1.0,
"zoneId" : "DXBZONE1",
"description" : "Masafi Park Al Quoz",
"locationPriority" : 1.0,
"accountTypeId" : 0.0,
"locationType" : "DEPOT",
"location" : {
"makani" : "",
"lat" : 25.123091,
"lng" : 55.21082
},
"deliveryDays" : "MTWRFSU",
"timeWindow" : {
"timeWindowTypeId" : "1"
},
"address1" : "",
"address2" : "",
"phone" : "",
"city" : "",
"county" : "",
"state" : "",
"country" : "",
"zipcode" : "",
"imageUrl" : "",
"contact" : {
"name" : "",
"email" : ""
},
"status" : "",
"createdBy" : "",
"updatedBy" : "",
"updateDate" : "",
"accountId" : 1.0,
"serviceTimeTypeId" : "1"
}
}
#5 楼
我不同意,如果我们在$ match阶段开始使用ID数组,就可以使$ lookup与ID数组一起使用。 // replace IDs array with lookup results
db.products.aggregate([
{ $match: { products : { $exists: true } } },
{
$lookup: {
from: "products",
localField: "products",
foreignField: "_id",
as: "productObjects"
}
}
])
如果要将查询结果传递给管道,它将变得更加复杂。但是又有一种方法可以做到这一点(@ user12164已建议):
// replace IDs array with lookup results passed to pipeline
db.products.aggregate([
{ $match: { products : { $exists: true } } },
{
$lookup: {
from: "products",
let: { products: "$products"},
pipeline: [
{ $match: { $expr: {$in: ["$_id", "$$products"] } } },
{ $project: {_id: 0} } // suppress _id
],
as: "productObjects"
}
}
])
#6 楼
使用$lookup
和后续的$group
进行聚合非常麻烦,因此,如果您使用的是node&Mongoose或架构中带有一些提示的支持库(这是一种中等的选择),则可以使用.populate()
来获取这些文档: var mongoose = require("mongoose"),
Schema = mongoose.Schema;
var productSchema = Schema({ ... });
var orderSchema = Schema({
_id : Number,
products: [ { type: Schema.Types.ObjectId, ref: "Product" } ]
});
var Product = mongoose.model("Product", productSchema);
var Order = mongoose.model("Order", orderSchema);
...
Order
.find(...)
.populate("products")
...
评论
我的订单文件示例不够清楚吗?您想要产品的示例文档吗?SERVER-22881将跟踪使数组正常工作(而不是文字值)。