如何在 MongoDB 中使用 $lookup

MongoDB 中 $lookup 的深入解析

MongoDB 是一种广泛应用的 NoSQL 数据库,它采用集合的方式来组织数据。一个 MongoDB 集合由多个文档构成,这些文档以 JSON 格式存储实际数据。你可以将文档类比为传统关系型数据库中的行,而集合则类似于表。

数据库的核心功能之一是查询其中存储的数据。数据查询允许我们检索特定信息,进行数据分析,生成报告,以及实现数据集成。

为了高效地查询数据库,我们必须能够将来自多个表(对于 SQL 数据库)或多个集合(对于 NoSQL 数据库)的数据合并成一个统一的结果集。

在 MongoDB 中,$lookup 操作符允许我们在查询过程中整合来自两个集合的信息。它执行类似于 SQL 数据库中的左外连接的操作。

$lookup 的应用场景和目标

数据库的一个重要用途是对数据进行处理,从而从原始数据中提取有价值的信息。例如,如果你经营一家餐厅,你可能需要分析餐厅的运营数据,以了解每天的收入、周末需要准备的菜品,甚至了解每天每个小时的咖啡销量。

对于此类需求,简单的数据库查询是远远不够的。你需要对存储的数据执行更高级的查询。为了满足这些需求,MongoDB 提供了聚合管道这一功能。

聚合管道是一个由多个可组合的操作(称为阶段)组成的系统,用于处理数据并生成最终的聚合结果。聚合管道中的阶段包括 $sort、$match、$group、$merge、$count 和 $lookup 等。

这些阶段可以按照任何顺序在聚合管道中使用。在聚合管道的每个阶段,会对通过管道传递的数据执行不同的操作。

因此,$lookup 是 MongoDB 聚合管道中的一个阶段。它用于在 MongoDB 数据库中的两个集合之间执行左外连接。左外连接会将左侧集合中的所有文档与右侧集合中匹配的文档合并在一起。

为了更好地理解,我们考虑以下两个集合,它们以表格形式呈现:

订单集合:

order_id customer_id order_date total_amount
1 100 2022-05-01 50.00
2 101 2022-05-02 75.00
3 102 2022-05-03 100.00

客户集合:

customer_num customer_name customer_email customer_phone
100 John [email protected] [email protected]

如果我们使用 `order_collection` 中的 `customer_id` 字段对上述集合进行左外连接(其中 `order_collection` 是左侧集合,`customers_collection` 是右侧集合),结果将包含 `Orders` 集合中的所有文档,以及 `Customers` 集合中 `customer_num` 与 `Orders` 集合中任何记录的 `customer_id` 相匹配的文档。

当以表格格式呈现时,对订单和客户集合进行左外连接操作的最终结果如下所示:

请注意,对于 `Orders` 集合中 `customer_id` 为 101 的客户,由于其在 `Customers` 集合中没有匹配的 `customer_num` 值,客户表中缺失的相应值已用空值填充。

$lookup 在字段之间执行严格的相等比较,并检索匹配的整个文档,而不仅仅是匹配的字段。

$lookup 的语法

$lookup 的基本语法如下:

   {
    $lookup: {
    from: <要连接的集合>,
    localField: <输入文档中的字段>,
    foreignField: <来自 "from" 集合的字段>,
    as: <输出数组字段>
    }
   }
  

$lookup 包含四个关键参数:

  • from:指定要从中查找文档的集合。在之前的示例中,即 `customers_collection`。
  • localField:当前操作集合(或主集合)中的一个字段,用于与 `from` 集合(例如 `customers_collection`)中的字段进行比较。在上述例子中,`localField` 将是 `orders_collection` 中的 `customer_id`。
  • foreignField:指定要与 `from` 集合中的 `localField` 进行比较的字段。在示例中,它将是 `customer_collection` 中的 `customer_num`。
  • as:新字段的名称,用于表示匹配结果,该结果是一个包含所有匹配文档的数组。如果没有匹配项,该字段将包含一个空数组。

继续我们之前的两个集合,我们将使用以下代码对两个集合执行 $lookup 操作,其中 `orders_collection` 作为我们的工作或主集合:

   {
    $lookup: {
     from: "customers_collection",
     localField: "customer_id",
     foreignField: "customer_num",
     as: "customer_info"
    }
  }
  

请注意,`as` 字段可以使用任何字符串值。然而,如果指定的名称已存在于工作文档中,该字段将被覆盖。

连接来自多个集合的数据

MongoDB 的 $lookup 是聚合管道中一个非常实用的阶段。虽然聚合管道不一定需要使用 $lookup 阶段,但在执行需要跨多个集合连接数据的复杂查询时,此阶段至关重要。

$lookup 阶段对两个集合执行左外连接,这将导致创建新的字段,或现有字段的值被来自另一个集合的文档数组覆盖。

这些文档是根据其值是否与要进行比较的字段的值相匹配来选择的。最终结果是一个包含文档数组的字段(如果找到匹配项),或者一个空数组(如果没有找到匹配项)。

考虑以下显示的员工和项目集合。

我们可以使用以下代码连接这两个集合:

  db.projects.aggregate([
    {
      $lookup: {
        from: "employees",
        localField: "employees",
        foreignField: "_id",
        as: "assigned_employees"
      }
    }
  ])
  

这个操作的结果是两个集合的组合。结果是项目及其分配给每个项目的所有员工。员工信息以数组形式呈现。

可以与 $lookup 一起使用的管道阶段

正如之前提到的,$lookup 是 MongoDB 聚合管道中的一个阶段,可以与其他聚合管道阶段配合使用。为了演示这些阶段如何与 $lookup 一起使用,我们将使用以下两个集合进行说明。

在 MongoDB 中,它们以 JSON 格式存储。这就是上述集合在 MongoDB 中的样子。

以下是一些可以与 $lookup 一起使用的聚合管道阶段的示例:

$match

$match 是一个聚合管道阶段,用于过滤文档流,只允许满足给定条件的文档进入聚合管道的下一阶段。此阶段最好在管道的早期使用,以删除不需要的文档,从而优化聚合管道。

使用前面的两个集合,您可以像这样组合 $match 和 $lookup:

  db.users.aggregate([
    {
      $match: {
       country: "USA"
      }
    },
    {
      $lookup: {
        from: "orders",
        localField: "_id",
        foreignField: "user_id",
        as: "orders"
      }
    }
  ])
  

$match 用于过滤来自美国的用户。然后,将 $match 的结果与 $lookup 结合,以获取来自美国用户的订单详细信息。上述操作的结果如下图所示:

$project

$project 是一个阶段,通过指定要包含、排除或添加到文档的字段来重塑文档。例如,如果您正在处理每个包含十个字段的文档,但文档中只有四个字段包含数据处理所需的数据,则可以使用 $project 过滤掉不需要的字段。

这使您可以避免将不必要的数据发送到聚合管道的下一阶段。

我们可以像这样组合 $lookup 和 $project:

   db.users.aggregate([
    {
      $lookup: {
        from: "orders",
        localField: "_id",
        foreignField: "user_id",
        as: "orders"
      }
    },
    {
      $project: {
        name: 1,
        _id: 0,
        total_spent: { $sum: "$orders.price" }
      }
    }
   ])
  

上面的代码将 `users` 和 `orders` 集合与 $lookup 结合使用,然后使用 $project 仅显示每个用户的姓名和花费的总金额。$project 还用于从结果中删除 `_id` 字段。上述操作的结果如下图所示:

$unwind

$unwind 是一个聚合阶段,用于解构或展开数组字段,为数组中的每个元素创建新文档。如果您想对数组字段值运行一些聚合,这将非常有用。

例如,在下面的例子中,如果您想在 `hobbies` 字段上运行聚合,您不能这样做,因为它是一个数组。但是,您可以使用 $unwind 展开它,然后对生成的文档执行聚合。

使用 `users` 和 `orders` 集合,我们可以像这样一起使用 $lookup 和 $unwind:

   db.users.aggregate([
    {
      $lookup: {
        from: "orders",
        localField: "_id",
        foreignField: "user_id",
        as: "orders"
      }
    },
    {
      $unwind: "$orders"
    }
  ])
  

在上面的代码中,$lookup 返回一个名为 `orders` 的数组字段。然后,使用 $unwind 展开数组字段。此操作的结果如下所示:注意 Alice 出现了两次,因为她有两个订单。

$lookup 用例示例

在进行数据处理时,$lookup 是一个非常有用的工具。例如,您可能有两个需要根据具有相似数据的集合中的字段加入的集合。可以使用一个简单的 $lookup 阶段来执行此操作,并在主集合中添加一个新字段,其中包含从另一个集合获取的文档。

考虑如下所示的用户和订单集合:

可以使用 $lookup 组合这两个集合,得到如下所示的结果:

$lookup 也可用于执行更复杂的连接。$lookup 不仅限于对两个集合执行连接。您可以实施多个 $lookup 阶段,以对两个以上的集合执行连接。考虑以下三个集合:

我们可以使用下面的代码,在三个集合之间执行更复杂的连接,以获取所有已下订单以及已订购产品的详细信息。

下面的代码允许我们这样做:

   db.orders.aggregate([
    {
      $lookup: {
        from: "order_items",
        localField: "_id",
        foreignField: "order_id",
        as: "order_items"
      }
    },
    {
      $unwind: "$order_items"
    },
    {
      $lookup: {
        from: "products",
        localField: "order_items.product_id",
        foreignField: "_id",
        as: "product_details"
      }
    },
    {
      $group: {
        _id: "$_id",
        customer: { $first: "$customer" },
        total: { $sum: "$order_items.price" },
        products: { $push: "$product_details" }
      }
    }
   ])
  

上述操作的结果如下图所示:

总结

在执行涉及多个集合的数据处理时,$lookup 非常有用,因为它允许您连接数据,并根据存储在多个集合中的数据得出结论。数据处理很少只依赖于单个集合。

要从数据中得出有意义的结论,连接多个集合中的数据至关重要。因此,考虑在 MongoDB 聚合管道中使用 $lookup 阶段,以更好地处理数据,并从跨多个集合存储的原始数据中得出有价值的见解。

你也可以进一步探索一些 MongoDB 的命令和查询。