mongodb $lookup 运算符

有了 $lookup 之后,MongoDB 也能做关联查询了,典型的案例就是商品和订单,订单里面有商品,模型如下:

商品模型

  • _id:唯一标识符
  • name:商品名称
  • price:商品价格

创建三条商品数据:

1
2
3
db.products.insert({name:'矿泉水', price:1}) // 5b120c565c5d2ea98f402c06
db.products.insert({name:'菜园小饼', price:4}) // 5b120c565c5d2ea98f402c07
db.products.insert({name:'挂面', price:5}) // 5b120f995c5d2ea98f402c0b

订单模型

  • _id:唯一标识符
  • user:购买人(这里为了演示方便,就用字符串保存姓名)
  • product:购买的商品(引用类型,存商品模型的 ObjectId)
  • createdAt:购买时间

创建四条订单数据:

1
2
3
4
db.orders.insert({user:'张三', product:ObjectId('5b120c565c5d2ea98f402c06'), createdAt:new Date()})
db.orders.insert({user:'张三', product:ObjectId('5b120c565c5d2ea98f402c07'), createdAt:new Date()})
db.orders.insert({user:'李四', product:ObjectId('5b120c565c5d2ea98f402c07'), createdAt:new Date()})
db.orders.insert({user:'王五', product:ObjectId('5b120f995c5d2ea98f402c0b'), createdAt:new Date()})

现在的需求是:

查找所有产品价格大于 2 的订单列表。

先口算一下:张三买了水和菜园小饼,李四只买了菜园小饼,王五买了挂面。所以需求所要的订单列表就是上面创建的四条订单中的后三条。接下来就看如何用 MongoDB 的方式来实现这个查询。

  1. 第一种思路是分两次查询。

    首先锁定价格大于2的商品:

    1
    2
    3
    4
    5
    6
    db.products.find({price:{$gt:2}}).map(it=>it._id)
    /* 1 */
    [
    ObjectId("5b120c565c5d2ea98f402c07"),
    ObjectId("5b120f995c5d2ea98f402c0b")
    ]

    然后再根据这些商品的 _id 过滤订单。

    1
    2
    3
    4
    5
    db.orders.find({
    product: {
    $in: [ ObjectId("5b120c565c5d2ea98f402c07"), ObjectId("5b120f995c5d2ea98f402c0b") ]
    }
    })

    最后得到结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /* 1 */
    {
    "_id" : ObjectId("5b120cf65c5d2ea98f402c09"),
    "user" : "张三",
    "product" : ObjectId("5b120c565c5d2ea98f402c07"),
    "createdAt" : ISODate("2018-06-02T03:20:22.936Z")
    }

    /* 2 */
    {
    "_id" : ObjectId("5b120cff5c5d2ea98f402c0a"),
    "user" : "李四",
    "product" : ObjectId("5b120c565c5d2ea98f402c07"),
    "createdAt" : ISODate("2018-06-02T03:20:31.571Z")
    }

    /* 3 */
    {
    "_id" : ObjectId("5b120fe25c5d2ea98f402c0c"),
    "user" : "王五",
    "product" : ObjectId("5b120f995c5d2ea98f402c0b"),
    "createdAt" : ISODate("2018-06-02T03:32:50.920Z")
    }

    可以看到,查询结果就是订单的后三条数据,与口算结果完全吻合。

  2. 第二种思路是根据 orders 聚合。

    用 $lookup 把订单中所有商品给 populate 进去:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    db.orders.aggregate([
    {
    $lookup:{
    from:'products',
    localField:"product",
    foreignField:"_id",
    as:"product"
    }
    }
    ])

    然后用 $match 找到匹配的订单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    db.orders.aggregate([
    {
    $lookup:{
    from:'products',
    localField:"product",
    foreignField:"_id",
    as:"product"
    }
    },
    {
    $match: {
    'product.price' : {$gt: 2}
    }
    }
    ])

    最后得到结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /* 1 */
    {
    "_id" : ObjectId("5b120cf65c5d2ea98f402c09"),
    "user" : "张三",
    "product" : [
    {
    "_id" : ObjectId("5b120c565c5d2ea98f402c07"),
    "name" : "菜园小饼",
    "price" : 4.0
    }
    ],
    "createdAt" : ISODate("2018-06-02T03:20:22.936Z")
    }

    /* 2 */
    {
    "_id" : ObjectId("5b120cff5c5d2ea98f402c0a"),
    "user" : "李四",
    "product" : [
    {
    "_id" : ObjectId("5b120c565c5d2ea98f402c07"),
    "name" : "菜园小饼",
    "price" : 4.0
    }
    ],
    "createdAt" : ISODate("2018-06-02T03:20:31.571Z")
    }

    /* 3 */
    {
    "_id" : ObjectId("5b120fe25c5d2ea98f402c0c"),
    "user" : "王五",
    "product" : [
    {
    "_id" : ObjectId("5b120f995c5d2ea98f402c0b"),
    "name" : "挂面",
    "price" : 5.0
    }
    ],
    "createdAt" : ISODate("2018-06-02T03:32:50.920Z")
    }

    可以看到不仅所要的订单列表被查出来了,每一条订单购买的商品详情也被注入到列表中。

  3. 第三种思路是根据 products 进行聚合。

    首先筛选价格大于 2 的所有商品。

    1
    2
    3
    4
    5
    6
    7
    db.products.aggregate([
    {
    $match: {
    price : {$gt: 2}
    }
    }
    ])

    然后把商品对应的所有订单通过 $lookup 查出来,赋值给商品的 orders 字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    db.products.aggregate([
    {
    $match: {
    price : {$gt: 2}
    }
    },
    {
    $lookup:{
    from:'orders',
    localField:"_id",
    foreignField:"product",
    as:"orders"
    }
    }
    ])

    这个得到的是商品列表,而我们需要的是订单列表,要通过$project 保留 orders 字段,然后用 $unwind 展开数组,最后再用 $replaceRoot 去掉 orders 键,保留订单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    db.products.aggregate([
    {
    $match: {
    price : {$gt: 2}
    }
    },
    {
    $lookup:{
    from:'orders',
    localField:"_id",
    foreignField:"product",
    as:"orders"
    }
    },
    {
    $project: {
    orders: 1,
    _id: 0
    }
    },
    {
    $unwind : "$orders"
    }
    { $replaceRoot: { newRoot: "$orders" }}
    ])

    不过需要注意的是:$replaceRoot 只在 3.4 版本以上才被支持,之前的版本只能用 js 的 map 来达到类似的效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    db.products.aggregate([
    {
    $match: {
    price : {$gt: 2}
    }
    },
    {
    $lookup:{
    from:'orders',
    localField:"_id",
    foreignField:"product",
    as:"orders"
    }
    },
    {
    $project: {
    orders:1,
    _id:0
    }
    },
    {
    $unwind : "$orders"
    }
    ]).map(it=>it.orders)

上面 3 中方法效率如何呢?没有测试过,但我估计第二种效率会低一些,因为它是先 $lookup 拼接新表之后,再进行过滤的。第一种和第三种不知道谁效率更高,前者更直观但需要查两次,后者一步到位但语法更晦涩。在使用 $lookup 的时候,推荐按照下面的步骤:

  1. $match 先根据条件过滤
  2. $project 保留想要的字段
  3. $unwind 展开数组
  4. $lookup 关联数据