MongoDB中多表关联查询($lookup)的深入讲解
一. 聚合框架
聚合框架是MongoDB的高级查询语言,它允许我们通过转换和合并多个文档中的数据来生成新的单个文档中不存在的信息。
聚合管道操作主要包含下面几个部分:
$lookup
命令
功能描述
$project
指定输出文档里的字段.
$match
选择要处理的文档,与fine()类似。
$limit
限制传递给下一步的文档数量。
$skip
跳过一定数量的文档。
$unwind
扩展数组,为每个数组入口生成一个输出文档。
$group
根据key来分组文档。
$sort
排序文档。
$geoNear
选择某个地理位置附近的的文档。
$out
把管道的结果写入某个集合。
$redact
控制特定数据的访问。
多表关联(3.2版本新增)
在本篇幅中,我们聚焦$lookup的使用。
二. $lookup的功能及语法
1.主要功能是将每个输入待处理的文档,经过$lookup阶段的处理,输出的新文档中会包含一个新生成的数组列(户名可根据需要命名新key的名字)。数组列存放的数据是来自被Join集合的适配文档,如果没有,集合为空(即为[])
2.基本语法
{ $lookup: { from:, localField: , foreignField: , as: } }
3.语法的解释说明
源集合中的match值,如果输入的集合中,某文档没有 localField 这个Key(Field),在处理的过程中,会默认为此文档含 有localField:null的键值对。
语法值
解释说明
from
同一个数据库下等待被Join的集合。
localField
foreignField
待Join的集合的match值,如果待Join的集合中,文档没有foreignField
as
为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉,
(注:null=null此为真)
其语法功能类似于下面的伪SQL语句:
SELECT*,FROMcollection WHERE IN(SELECT* FROM WHERE = );
三.案例
以上的语法介绍有些枯燥,不易理解,我们直接分析品味案例好了。
假设有订单集合,存储的测试数据如下:
db.orders.insert([ {"_id":1,"item":"almonds","price":12,"quantity":2}, {"_id":2,"item":"pecans","price":20,"quantity":1}, {"_id":3} ])
其中item对应数据为商品名称。
另外一个就是就是商品库存集合,存储的测试数据如下:
db.inventory.insert([ {"_id":1,"sku":"almonds",description:"product1","instock":120}, {"_id":2,"sku":"bread",description:"product2","instock":80}, {"_id":3,"sku":"cashews",description:"product3","instock":60}, {"_id":4,"sku":"pecans",description:"product4","instock":70}, {"_id":5,"sku":null,description:"Incomplete"}, {"_id":6} ])
此集合中的sku数据等同于订单集合中的商品名称。
在这种模式设计下,如果要查询订单表对应商品的库存情况,应如何写代码呢?
很明显这需要两个集合Join。
场景简单,不做赘述,直送答案。其语句如下:
db.orders.aggregate([ { $lookup: { from:"inventory", localField:"item", foreignField:"sku", as:"inventory_docs" } } ])
返回的执行结果如下:
{
"_id":NumberInt("1"),
"item":"almonds",
"price":NumberInt("12"),
"quantity":NumberInt("2"),
"inventory_docs":[
{
"_id":NumberInt("1"),
"sku":"almonds",
"description":"product1",
"instock":NumberInt("120")
}
]
}
{
"_id":NumberInt("2"),
"item":"pecans",
"price":NumberInt("20"),
"quantity":NumberInt("1"),
"inventory_docs":[
{
"_id":NumberInt("4"),
"sku":"pecans",
"description":"product4",
"instock":NumberInt("70")
}
]
}
{
"_id":NumberInt("3"),
"inventory_docs":[
{
"_id":NumberInt("5"),
"sku":null,
"description":"Incomplete"
},
{
"_id":NumberInt("6")
}
]
}
分析查询语句和结果,回扣$lookup定义,可以将上面的处理过程,描述如下:
从集合order中逐个获取文档处理,拿到一个文档后,会根据localField值遍历被Join的inventory集合(from:"inventory"),看inventory集合文档中foreignField值是否与之相等。如果相等,就把符合条件的inventory文档 整体内嵌到聚合框架新生成的文档中,并且新key统一命名为inventory_docs。考虑到符合条件的文档不唯一,这个Key对应的Value是个数组形式。原集合中Key对应的值为Null值或不存在时,需特别小心。
四.说明
在以下的说明中,为描述方便,将from对应的集合定义为被join集合;待聚合的表成为源表;将localField和foreignField对应的Key定义比较列。
1.因客户端工具显示的问题,上面示例中查询结果重Int类型值都自动显示为了NumberInt("")。这个NumberInt标注,请忽略,不影响我们的功能测试。
2.这个示例中,一共输出了三个文档,在没有再次聚合($match)的条件下,这个输出文档数量是以输入文档的数量来决定的(由order来决定),而不是以被Join的集合(inventory)文档数量决定。
3.在此需要特别强调的是输出的第三个文档。在源库中原文档没有要比较的列(即item值不存在,既不是Null值,也不是值为空),此时和被Join集合比较,如果被Join集合中比较列也恰好为NUll或不存在的值,此时,判断相等,即会把被Join集合中比较列为NUll或值不存在文档吸收进来。
4.假设源表(order)中比较列为某一个值,而此值在待比较表(inventory)的所有文档中都不存在,那么查询结果会是什么样子呢?
order集合在现有数据的基础上,再被insert进一笔测试数据,这个订单的商品为Start,在库存商品中根本没有此数据。
db.orders.insert({"_id":4,"item":"Start","price":2000,"quantity":1})
order集合的文档数量由之前的3个增长为4个。
再次执行查询
db.orders.aggregate([ { $lookup: { from:"inventory", localField:"item", foreignField:"sku", as:"inventory_docs" } } ])
此时查看结果
{
"_id":NumberInt("1"),
"item":"almonds",
"price":NumberInt("12"),
"quantity":NumberInt("2"),
"inventory_docs":[
{
"_id":NumberInt("1"),
"sku":"almonds",
"description":"product1",
"instock":NumberInt("120")
}
]
}{
"_id":NumberInt("2"),
"item":"pecans",
"price":NumberInt("20"),
"quantity":NumberInt("1"),
"inventory_docs":[
{
"_id":NumberInt("4"),
"sku":"pecans",
"description":"product4",
"instock":NumberInt("70")
}
]
}
{
"_id":NumberInt("3"),
"inventory_docs":[
{
"_id":NumberInt("5"),
"sku":null,
"description":"Incomplete"
},
{
"_id":NumberInt("6")
}
]
}
{
"_id":NumberInt("4"),
"item":"Start",
"price":NumberInt("2000"),
"quantity":NumberInt("1"),
"inventory_docs":[]
}
查询出的结果也由之前的3个变成了4个。比较特别的是第四个文档,其新增列为"inventory_docs":[],即值为空。所以,此时,实现的功能非常像关系型数据库的leftjoin。
那么,可不可以只筛选出新增列为空的文档呢?
即我们查询出,比较列的条件下,刷选出只在A集合中,而不在集合B的文档呢?就像关系数据库中量表Join的leftjoinona.key=b.keywhereb.keyisnull.
答案是可以的。
其实回到聚合框架上来,再次聚合一下就可以了,来一次$match就可以了。
执行的语句调整一下就OK了。
db.orders.aggregate([ { $lookup: { from:"inventory", localField:"item", foreignField:"sku", as:"inventory_docs" } }, {$match:{"inventory_docs":[]}} ])
执行结果为
{
"_id":NumberInt("4"),
"item":"Start",
"price":NumberInt("2000"),
"quantity":NumberInt("1"),
"inventory_docs":[]
}
可以看出执行结果只有一个文档。这个文档表明的含义是:订单中有这个商品,但是库存中没有这个商品。
($look只是聚合框架的一个stage,在其前前后后,都可以嵌入到其他的聚合管道的命令,例如$match.$group等。下面的说明5,也可以说明一二)
5.以上的比较列都是单一的Key/Value,如果复杂一点,如果比较的列是数组,我们又该如何关联呢?
我们接下来再来测试一把。将之前集合order、inventory插入的数据清空。
插入此场景下的新数据,向order中插入的数据,如下:
db.orders.insert({"_id":1,"item":"MON1003","price":350,"quantity":2,"specs":["27inch","Retinadisplay","1920x1080"],"type":"Monitor"})
specs对应的value是数组格式。
向集合inventory新插入的数据如下:
db.inventory.insert({"_id":1,"sku":"MON1003","type":"Monitor","instock":120,"size":"27inch","resolution":"1920x1080"}) db.inventory.insert({"_id":2,"sku":"MON1012","type":"Monitor","instock":85,"size":"23inch","resolution":"1280x800"}) db.inventory.insert({"_id":3,"sku":"MON1031","type":"Monitor","instock":60,"size":"23inch","display_type":"LED"})
查询的语句如下:
db.orders.aggregate([ { $unwind:"$specs" }, { $lookup: { from:"inventory", localField:"specs", foreignField:"size", as:"inventory_docs" } }, { $match:{"inventory_docs":{$ne:[]}} } ])
查询显示结果如下:
{
"_id":NumberInt("1"),
"item":"MON1003",
"price":NumberInt("350"),
"quantity":NumberInt("2"),
"specs":"27inch",
"type":"Monitor",
"inventory_docs":[
{
"_id":NumberInt("1"),
"sku":"MON1003",
"type":"Monitor",
"instock":NumberInt("120"),
"size":"27inch",
"resolution":"1920x1080"
}
]
}
仔细看啊,输出文档中的specs对应的数据变成了字符串类型(原集合为数组)。是什么发挥了如此神奇功效???,请看黑板,请将目光集中在
{ $unwind:"$specs" }
还有个小问题,大家猜一下,如果查询语句中没有
{ $match:{"inventory_docs":{$ne:[]}} }
结果会是什么样呢?即查看语句修改为:
db.orders.aggregate([ { $unwind:"$specs" }, { $lookup: { from:"inventory", localField:"specs", foreignField:"size", as:"inventory_docs" } } ])
大家猜猜看!
大家猜猜看!
大家猜猜看!
呵呵...此时的结果是:
文档1
{
"_id":NumberInt("1"),
"item":"MON1003",
"price":NumberInt("350"),
"quantity":NumberInt("2"),
"specs":"27inch",
"type":"Monitor",
"inventory_docs":[
{
"_id":NumberInt("1"),
"sku":"MON1003",
"type":"Monitor",
"instock":NumberInt("120"),
"size":"27inch",
"resolution":"1920x1080"
}
]
}文档2
{
"_id":NumberInt("1"),
"item":"MON1003",
"price":NumberInt("350"),
"quantity":NumberInt("2"),
"specs":"Retinadisplay",
"type":"Monitor",
"inventory_docs":[]
}文档3
{
"_id":NumberInt("1"),
"item":"MON1003",
"price":NumberInt("350"),
"quantity":NumberInt("2"),
"specs":"1920x1080",
"type":"Monitor",
"inventory_docs":[]
}
你推算出正确结果了吗?
谢谢!!!
希望以上的讲解和演示能对大家学习$lookup有所帮助。
注:以上案例数据参考MongoDB官方网站,大家也可访问官网获取更多、更全的相关知识。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。