MongoDB 聚合管道中的 $lookup 关联查询
javascript { $lookup: { from: <要连接的集合名称>, localField: <当前集合中的字段>, foreignField: <目标集合中的字段>, as: <输出数组中存放连接结果的新字段名> } }
对于更复杂的关联条件,MongoDB 3.6+ 提供了带有嵌套管道(pipeline)的语法:
```javascript
{
$lookup: {
from: <目标集合>,
let: { <变量1>: <表达式>, ... },
pipeline: [ <在目标集合上执行的管道阶段> ],
as: <输出字段名>
}
}
参数说明
| 参数 | 说明 |
|---|---|
from |
要关联的集合名称,必须是同一数据库中的集合。 |
localField |
当前集合并入连接条件的字段。 |
foreignField |
目标集合中用于连接的字段。 |
as |
存放匹配结果的新数组字段名称。如果当前文档中已存在该字段,则会被覆盖。 |
let (可选) |
定义在管道中使用的变量,变量的值可以来自当前文档的字段。 |
pipeline (可选) |
在目标集合上执行的聚合管道,用于过滤、投射、转换等操作。注意不能包含 $out 或 $merge 阶段。 |
基础示例:一对多关联
假设有两个集合:orders(订单)和 customers(客户)。
orders 文档结构:
{ "_id": 1, "item": "笔记本", "price": 8000, "customerId": 101 }
{ "_id": 2, "item": "鼠标", "price": 150, "customerId": 102 }
customers 文档结构:
{ "_id": 101, "name": "张三", "email": "zhangsan@example.com" }
{ "_id": 102, "name": "李四", "email": "lisi@example.com" }
查询每个订单及其对应的客户信息:
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
}
])
返回结果:
[
{
"_id": 1,
"item": "笔记本",
"price": 8000,
"customerId": 101,
"customer": [
{ "_id": 101, "name": "张三", "email": "zhangsan@example.com" }
]
},
{
"_id": 2,
"item": "鼠标",
"price": 150,
"customerId": 102,
"customer": [
{ "_id": 102, "name": "李四", "email": "lisi@example.com" }
]
}
]
customer 始终是一个数组,即使只匹配到一个文档。如果目标集合中没有匹配的文档,数组为空 []。
进阶用法:使用 pipeline 实现复杂关联
当简单的等值连接无法满足需求时,比如需要连接多个条件、对目标集合进行过滤或字段转换,可以使用带 pipeline 的 $lookup。
示例:连接时过滤目标文档
依然使用 orders 集合,但这次我们只想取回状态为“active”的客户。
customers 集合增加一个 status 字段:
{ "_id": 101, "name": "张三", "email": "zhangsan@example.com", "status": "active" }
{ "_id": 102, "name": "李四", "email": "lisi@example.com", "status": "inactive" }
查询:
db.orders.aggregate([
{
$lookup: {
from: "customers",
let: { custId: "$customerId" },
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$custId"] }, status: "active" } },
{ $project: { _id: 0, name: 1, email: 1 } }
],
as: "customer"
}
}
])
说明:
let定义了变量custId,其值为当前文档的customerId。- 在
pipeline中,$match使用$expr结合$$custId引用外部变量,并额外过滤status。 $project仅返回需要的字段。
这样,如果客户是 inactive 状态,customer 数组就会是空。
多字段关联与数组关联
有时连接条件不止一个字段,或者目标字段是数组,同样可以用 pipeline 优雅解决。
示例:多字段连接
假设 orders 和 customers 需要通过 customerId 和 region 字段共同关联。
db.orders.aggregate([
{
$lookup: {
from: "customers",
let: { o_custId: "$customerId", o_region: "$region" },
pipeline: [
{ $match: {
$expr: {
$and: [
{ $eq: ["$_id", "$$o_custId"] },
{ $eq: ["$region", "$$o_region"] }
]
}
}
}
],
as: "customer"
}
}
])
示例:foreignField 是数组
如果目标集合的关联字段是数组,不能直接用简单的 localField / foreignField 语法(那只适用于单值字段),需要使用 pipeline 和 $in。
customers 集合中 orderIds 是一个数组:
{ "_id": 101, "name": "张三", "orderIds": [1, 3, 4] }
反向查找某个订单所属的客户:
db.orders.aggregate([
{
$lookup: {
from: "customers",
let: { orderId: "$_id" },
pipeline: [
{ $match: { $expr: { $in: ["$$orderId", "$orderIds"] } } }
],
as: "customer"
}
}
])
$lookup 与 $unwind 配合
由于 $lookup 产生的关联字段是一个数组,如果确定每条记录最多只有一个匹配结果(例如一对一或一对一并入),我们通常使用 $unwind 将数组展开为单个对象,方便后续处理。
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" }
])
更安全的方式是使用带 preserveNullAndEmptyArrays 选项的 $unwind,避免因未匹配到文档而导致整条文档被丢弃。
{ $unwind: { path: "$customer", preserveNullAndEmptyArrays: true } }