HOWTO · MongoDB

使用 MongoDB `$lookup` 管道连接文档

使用 MongoDB `$lookup` 管道连接文档:按 `shopId` 匹配,并要求目标文档中缺少某个字段;还会说明如何筛除未匹配的结果。

本页内容

在 MongoDB 的 $lookup 阶段中使用管道:当 collection2 与 collection1 文档的 shopId 相同,且 collection1 文档没有 shopPosId 字段时,将这些文档连接起来。$lookup 执行的是左外连接,因此 collection2 的每条输入文档都会保留在结果中;没有匹配项的行,其 shopDescription 数组为空。若只想保留连接到文档的行,可以再添加一个 $match 阶段。

什么是聚合管道

聚合管道按顺序逐个阶段处理文档。每个阶段都可以筛选或调整输入文档的结构,并将结果传给下一阶段。在这个示例中,$lookup 会添加匹配的文档,后续的 $match 则可以筛选连接结果。MongoDB 还提供 map-reduce 和单一用途的聚合操作,但本次连接不需要它们。

更多详情请参阅 MongoDB 聚合管道概览。

MongoDB 中的 $lookup 运算符是什么

$lookup 会连接同一数据库中另一个集合的文档,并将匹配项写入一个数组字段。它的 from 选项指定要搜索的集合,as 选项指定要添加到每条输入文档中的数组字段名。使用管道形式时,可以对要连接的文档应用条件。

MongoDB $lookup 参考文档介绍了该运算符支持的语法形式。下面的示例使用相关管道:let 将输入文档中的值提供给该管道,$expr 则允许 $match 使用这个值比较字段。本文中的 MongoDB shell 命令是供读者在 MongoDB 数据库中运行的示例;配置的文章运行环境不会执行数据库操作。

在 MongoDB 中使用 $lookup 管道按条件连接文档

使用以下文档创建 collection1 和 collection2。这里用 MongoDB shell 展示初始化过程;请在 mongosh 中对要使用的数据库逐条运行这些语句。

db.createCollection('collection1'); db.createCollection('collection2'); db.collection1.insertMany([{"shopId":"001","shopPosId":"001","description":"description for 001"},{"shopId":"002","description":"description for 002"},{"shopId":"003","shopPosId":"003","description":"description for 003"},{"shopId":"004","description":"description for 004"}]); db.collection2.insertMany([{"shopId":"001","shopPosId":"0078","clientUid":"474192"},{"shopId":"002","shopPosId":"0012","clientUid":"474193"},{"shopId":"003","shopPosId":"0034","clientUid":"474194"},{"shopId":"004","shopPosId":"0056","clientUid":"474195"}]);

下面的独立 Python 示例模拟相同样本数据的匹配逻辑:对于商店 002 和 004,其 collection1 文档的 shopId 相匹配且没有 shopPosId,因此会连接到对应的描述。此示例仅用于说明结果逻辑,并不是 MongoDB 查询。示例及其输出中可见的文本(如 “description for 002”)保持英文,不作翻译。

运行此示例:

collection1 = [
    {"shopId": "001", "shopPosId": "001", "description": "description for 001"},
    {"shopId": "002", "description": "description for 002"},
    {"shopId": "003", "shopPosId": "003", "description": "description for 003"},
    {"shopId": "004", "description": "description for 004"},
]
collection2 = [
    {"shopId": "001", "shopPosId": "0078", "clientUid": "474192"},
    {"shopId": "002", "shopPosId": "0012", "clientUid": "474193"},
    {"shopId": "003", "shopPosId": "0034", "clientUid": "474194"},
    {"shopId": "004", "shopPosId": "0056", "clientUid": "474195"},
]

for source in collection2:
    matches = [
        item["description"]
        for item in collection1
        if item["shopId"] == source["shopId"] and "shopPosId" not in item
    ]
    print(f'{source["shopId"]}: {matches}')

此示例及其输出中的标签保持原样,仍为英文。

001: []
002: ['description for 002']
003: []
004: ['description for 004']

在 MongoDB 管道中,设置 let: {shopId: "$shopId"},将每条 collection2 记录的 ID 传入查找管道。使用 $expr: {$eq: ["$shopId", "$$shopId"]} 进行匹配;前缀 $$ 表示引用由 let 定义的变量。再通过 $and 将该比较与 {shopPosId: {$exists: false}} 组合起来,要求被连接的 collection1 文档中不存在该字段。

对应的第一条查询是 db.collection2.aggregate([{$lookup: {from: "collection1", let: {shopId: "$shopId"}, pipeline: [{$match: {$and: [{$expr: {$eq: ["$shopId", "$$shopId"]}}, {shopPosId: {$exists: false}}]}}], as: "shopDescription"}}])。它会保留 collection2 的全部四条输入记录。商店 001 和 003 没有符合条件的 collection1 文档,因此它们的 shopDescription 数组为空;商店 002 和 004 则因连接到的文档缺少 shopPosId 而匹配成功。

若只想返回有匹配项的行,可以追加一个 $match 阶段,检查连接数组是否至少包含一个元素:{$match: {shopDescription: {$ne: []}}}。完整的后续查询是 db.collection2.aggregate([{$lookup: {from: "collection1", let: {shopId: "$shopId"}, pipeline: [{$match: {$and: [{$expr: {$eq: ["$shopId", "$$shopId"]}}, {shopPosId: {$exists: false}}]}}], as: "shopDescription"}}, {$match: {shopDescription: {$ne: []}}}])。使用这些样本数据时,结果会返回商店 002 和 004 的源记录。第一个查找阶段仍然是左外连接;第二个阶段才会移除连接数组为空的行。