在数组中使用ObjectId进行$lookup操作

150

如何在 $lookup 中查询一个包含多个 ObjectId 的数组字段的语法,而不仅仅是单个 ObjectId?

示例订单文档:

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

我的订单文档示例不够清晰吗?您需要产品示例文档吗? - Jason Lin
SERVER-22881 将跟踪使数组按预期工作(而不是作为字面值)。 - Asya Kamsky
8个回答

188

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基本上是非常新的,目前它的工作方式对于那些熟悉mongoose中“穷人版”.populate()方法的人来说应该是很熟悉的。不同之处在于,$lookup提供了“服务器端”处理“连接”,而不是在客户端处理,并且一些在.populate()中已经成熟的功能在$lookup中目前还缺少(例如直接在数组上插值查找)。
这实际上是一个指定的改进问题SERVER-22881,因此有些运气的话,它将在下一个发布版本或之后的某个版本中得到解决。
作为设计原则,您当前的结构既不好也不坏,但在创建任何“连接”时都需要考虑开销。因此,MongoDB在创始时的基本原则适用,即如果您可以在一个集合中“预连接”数据,则最好这样做。
作为一个通用原则,$lookup 的另外一件事是,它的“join”意图是与这里展示的方式相反的。因此,最好的工作原则不是将其他文档的“相关 ID”保存在“父”文档中,而是将“相关文档”包含对“父文档”的引用。

可以说,$lookup 与像mongoose .populate()执行客户端连接所表现出的设计关系相反,因此与其通过先解开数组再进行查询,倒不如先识别每个"many"中的“one”,然后只需拉取相关项目即可实现。

1
@JasonLin 这不像“好/坏”那么直接,因此答案中添加了更多的解释。这取决于适合你什么。 - Blakes Seven
2
当前的实现有些不太合意。在本地字段数组中查找所有值是有意义的,但字面上使用该数组是没有意义的,因此SERVER-22881将跟踪修复这个问题。 - Asya Kamsky
@AsyaKamsky 那很有道理。我通常把关于 $lookup 和文档验证的查询视为功能处于初期,可能会得到改进。因此,直接扩展数组将受到欢迎,过滤结果的“查询”也将受到欢迎。这两个功能更符合许多人习惯的 mongoose.populate() 过程。将问题链接直接添加到答案内容中。 - Blakes Seven
非常感谢你,朋友。尽管在MongoDB中进行连接操作相对于关系型数据库解决方案来说仍然很困难,但MongoDB并不适用于这种操作。 - zubair1024
2
请注意,根据下面的答案,这已经得到实现,$lookup 现在可以直接用于数组。 - Adam Reis
显示剩余2条评论

55

34
您也可以使用“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 中)。

你也可以通过添加额外的 stage 来清理或投影外部记录,如上面的注释所示。


查阅关于lookup的let和pipeline参数的文档:https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#join-conditions-and-subqueries-on-a-joined-collection - Stefan
为什么没有$expr就不能工作? - madarinho

6

使用$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

我不太赞同,如果我们在$lookup阶段之前加上$match阶段,就可以将其与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"
        }
    }
])


这是最好的解决方案!非常感谢你! - Shariful Islam Mubin
很高兴能够帮忙 :) - Liebster Kamerad
1
尽管现代MongoDB允许直接使用数组进行$lookup,但是当尝试更改查找结果时(例如删除某些字段或将_id更改为id),此答案非常有帮助。感谢您的帮助。 - Oriol Miró

1

使用 $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")
    ...

0
如果您将产品的ID存储在字符串数组中,您可以使用以下代码:
db.users.aggregate([
  { $match: { _id: ObjectId(userId) } },
  {
    $addFields: {
      productIds: { $map: { input: "$productids", in: { $toObjectId: "$$this" } } }
    }
  },
  {
    $lookup: {
      from: "products",
      localField: "productIds",
      foreignField: "_id",
      as: "products"
    }
  },
  { $project: { "products.name": 1, _id: 0 } }
])

-1

在关系数据库术语中,查找基本上是左连接操作。

因此,左侧文档(在关系数据库术语中为表)是我们当前正在处理的文档,右侧文档是我们需要提取信息的文档。

查找->

   {
  $lookup:
     {
       from: <right document>,
       localField: <  field in left document which holds the info for right document >,
       foreignField: <field in the right document which is referenced in left document  >,
       as: <alias for array list where the result is to be stored.>
     }
}

所以上述问题的正确答案是 ->

{
"$lookup":
   {
        "from": "products",
        "localField": "products",
        "foreignField": "_id",
        "as": "productObject",
    }
}

但它对我来说没有起作用,我进行了调试并发现以下问题:因此,在我的情况下,在左侧文档中进行插入时我做错了什么是:

错误的插入 ->

产品文档 -->

{
  "_id": ObjectId(something),
  "products":[ "some_id_1", "some_id_2"]
}

正确的插入是 ->

产品文档

 {
  "_id": ObjectId(something),
  "products" : [ ObjectId("some_id_1"), ObjectId("some_id_2")]
 }

对我来说,我将对象ID存储为字符串,而不是在左侧文档的外键中作为ObjectId对象。

请确保在插入时使用正确的格式。

最后,我们都应该从别人的错误中学习。


这并没有回答如何使用ObjectId执行$lookup的问题。 - ray
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接