MongoDB是典型的NoSQL数据库,因此不提供JOIN操作。但有时我们仍然希望引用其他集合中的文档。最近使用到了MongoDB的联表查询,总结一下。
首先我们知道,在关系型数据库中,通过连接运算符可以实现多个表联合查询。而非关系型数据库的特点是数据是文档型,表内无模式,表之间属于弱关联,其本身特性不建议对多Collection关联处理,自然就很难在关系型数据库中非常擅长的多表关联上发挥作用。
MongoDB作为NoSQL代表,虽然属于非关系型数据库,但也是最像关系型数据库的一种NoSQL,提供了一些方法来解决此类问题。
建模和关系
MongoDB是非关系型数据库,具有灵活的模式。因为支持内嵌对象和数组类型,所以MongoDB建模也可以有两种方式,一种是内嵌(Embed),另一种是引用(References)。
而关系表示多个文档之间在逻辑上的相互联系,关系可以有:1:1(1对1)、1: N(1对多)、N: 1(多对1)、N: N(多对多)4种。文档间可以通过嵌入和引用来建立联系,也对应了MongoDB建模的两种方式。
接下来我们来以用户与用户地址的关系为例。一个用户可以有多个地址,所以是一对多的关系。
嵌入式关系
使用嵌入式方式(即内嵌)建模,我们可以把用户地址嵌入到用户的文档中:
1 | { |
以上数据保存在单一的文档中,可以比较容易的获取和维护数据。这种数据结构的缺点是,如果用户和用户地址在不断增加,数据量不断变大,会影响读写性能。
引用式关系
引用式关系是设计数据库时经常用到的方法,这种方法把用户数据文档和用户地址数据文档分开,通过引用文档的 id 字段来建立关系。
手动引用(Manual References)
1 | { |
以上实例中,用户文档的 address_ids 字段包含用户地址的对象id(ObjectId)数组。我们可以读取这些用户地址的对象id(ObjectId)来获取用户的详细地址信息。这种方法需要两次查询,第一次查询用户地址的对象id(ObjectId),第二次通过查询的id获取用户的详细地址信息。
1 | const result = db.users.findOne({"name":"Tom Benzamin"},{"address_ids":1}) |
DBRefs
考虑这样的一个场景,我们在不同的集合中(address_home,address_office,address_mailing等)存储不同的地址(住址,办公室地址,邮件地址等)。这样,我们在调用不同地址时,也需要指定集合,一个文档从多个集合引用文档,我们应该使用 DBRefs。
DBRef的形式:
1 | { $ref : , $id : , $db : } |
三个字段表示的意义为:
- $ref:集合名称
- $id:引用的id
- $db:数据库名称,可选参数
以下实例中用户数据文档使用了 DBRef, 字段 address:
1 | { |
address DBRef 字段指定了引用的地址文档是在 runoob 数据库下的 address_home 集合,id 为 534009e4d852427820000002。
以下代码中,我们通过指定 $ref 参数(address_home 集合)来查找集合中指定id的用户地址信息:
1 | const user = db.users.findOne({"name":"小明"}) |
以上实例返回了 address_home 集合中的地址数据:
1 | { |
综上,针对过于复杂或数据量很大的字段,我们使用引用而不是内嵌。并且,从广义上讲,这些是标准化数据模型。
$lookup
在MongoDb 3.2之前,我们需要使用DBRefs进行引用的设置。MongoDB 3.2 中增加了$lookup,而且放到了Aggregation(聚合)这种重量级的pipeline分析框架上。
MongoDB中Aggregation(聚合)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似sql语句中的 count(*)
。MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理,并且管道操作是可以重复的。举例说明:
1 | db.getCollection('courses').aggregate([{ |
以上语句首先声明以createtime
倒序排列,然后以courses
表为基础,合并了transcode_tasks
表联合查询,作为video字段,主键为id,外键为course_id,并且加入了筛选匹配了project_id
和audit.status
字段。MongoDB不是一个关系型数据库,但以上我们实际上使用$lookup实现了一次左连接操作。
populate
Mongoose提供了node.js中优雅的mongodb对象建模方案。MongoDB在版本> = 3.2中具有类似连接的$ lookup聚合运算符。而Mongoose有一个更强大的替代方法叫做populate()
,它允许你引用其他集合中的文档。
Population(填充)是使用来自其他集合的文档自动替换文档中的指定路径的过程。我们可以填充单个文档,多个文档,普通对象,多个普通对象或从查询返回的所有对象。我们来看一些例子。
外键引用
在Schema字段的定义中,可以添加ref
属性来指向另一个Schema。 该ref
属性在此后被填充(populate
)时将被读Mongoose取。 下面是存在互相引用的Person
与Story
的Schema定义。
1 | const mongoose = require('mongoose'), Schema = mongoose.Schema |
外键的类型可以是ObjectId
, Number
, String
, Buffer
中任何一种,在赋值与填充时保持一致即可。
保存与填充
Story
中保存Person
对象的_id
,此后在Query上调用.populate()
即可用Person
的文档来替换掉原来的字段。
1 | const alice = new Person({ _id: 0, name: 'Alice'}); |
更复杂的填充方式可以参考「在mongoose中填充外键」中的介绍。
动态填充
上文中调用.populate()
之前有一个条件:被填充的字段已被设置过ref
选项。Mongoose会去ref
指定的集合中去查找对应ID。 如果是动态字段怎么办?可以在填充的同时指定其ref
:
1 | const studentSchema = new Schema({ |