在数组中的ObjectId上的$ lookup


103

在由ObjectId数组而不是单个ObjectId组成的字段上执行$ lookup的语法是什么?

订单文档示例:

{
  _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>}
  ],
}

我的订单文件示例不够清楚吗?您想要产品的示例文档吗?
詹森·林

SERVER-22881将跟踪使数组正常工作(而不是文字值)。
Asya Kamsky

Answers:


139

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并且当前所.populate()提供的内容缺少某些“成熟度” (例如直接在数组上内插查找)。

这实际上是改进SERVER-22881的分配问题,因此如果运气好的话,它将在下一个版本或之后的某个版本发布。

作为一种设计原则,您当前的结构既不是好事,也不是坏事,而在创建任何“联接”时只会受到开销的影响。因此,MongoDB从一开始就具有基本的站立原则,如果您“可以”使用一个集合中的“预加入”数据,那么最好这样做。

可以说,$lookup作为一般原则的另一件事是,此处的“连接”的意图与此处显示的相反。因此,与其将其他文档的“相关标识”保留在“父”文档中,不如将最有效的一般原则是“相关文档”包含对“父”文档的引用。

因此,$lookup可以用一个“关系设计”是的像猫鼬如何反“最好的工作”这样说.populate()来执行它的客户端连接。通过在每个“许多”中标识“一个”,则只需拉入相关项,而无需先$unwind进入数组。


谢谢你的作品!这是否表明我的数据结构/规范化不正确?
Jason Lin

1
@JasonLin不如“好/不好”那样精明,因此在答案中添加了更多解释。这取决于什么适合您。
布雷克(Blakes)2016年

2
当前的实现是无意的。在本地字段数组中查找所有值是有意义的,从字面上使用该数组是没有意义的,因此SERVER-22881会跟踪该问题。
Asya Kamsky

@AsyaKamsky有意义。通常,我一直将查询$lookup和文档验证视为婴儿期的特征,并且可能会有所改善。因此,欢迎在数组上进行直接扩展,也可以使用“查询”来过滤结果。两者都将与.populate()许多人习惯的猫鼬过程更加一致。将问题链接直接添加到答案内容中。
布雷克七世

2
请注意,按照此答案下方的答案,此方法现已实现,$lookup现在可以直接在数组上使用。
亚当·里斯


15

您还可以使用该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也就是在local现场/道具products)。

您也可以使用其他stages 清理或投影外部记录,如上面的注释所示。


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"
    }
}

0

$lookup后续的聚合$group非常麻烦,因此,如果您使用的是node&Mongoose或在模式中带有一些提示的支持库(如果是的话,则为中等),您可以使用a .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")
    ...

0

我不得不不同意,如果我们在$ match阶段开始使用ID数组,就可以使$ lookup起作用。

// 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"
        }
    }
])

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.