mongoose关联查询技巧笔记

前言

数据库设计中数据之间的关联关系是极其常见的:一对一、一对多、多对多,作为 NoSQL 领头羊的 MongoDB 中常用做法无非「内嵌」和「引用」两种,因为 Document 有 16MB 的大小限制且「内嵌」不适合复杂的多对多关系,「引用」是用得更广泛的关联方式,所以 MongoDB 官方称其为“Normalized Data Models”——标准化数据模型。

引用式的关联其实很简单,指文档与文档之间通过_id字段的引用来进行关联。Mongoose 4.5.0版本以后提供了与 aggregate 功能写法都非常类似的virtual()方法,这里先不做对比,可查看另一篇笔记mongoose4.5之virtual虚拟值填充,本文要阐述的重点就在于如何去查这两个表通过aggregate 与 populate,并介绍4.5的aggregate的关联引用反向查询

populate

先说说populate吧,首先,Mongoose 的一切始于 Schema,使用 populate 的重点也在于 Schema 中的设置:

1
2
3
4
5
6
7
8
9
10
11
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const authorSchema = new Schema({
"author": String,
"age": Number,
"books": [{
type: Schema.Types.ObjectId,
ref: 'Book' // 关联的Model
}]
});
module.exports = mongoose.model("Author", authorSchema, "authors"); // 分别为Model名、Schema、数据库中集合名

使用

1
2
3
let result = await Author.find({
"author": "Zander"
}).populate("books");

返回如下数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[
{
"books": [
{
"_id": "5dccfcb5a3fab06c89020c8d",
"name": "代码的弱点"
},
{
"_id": "5dccfd30a3fab06c89020caa",
"name": "代码与六便士"
},
{
"_id": "5dccfda6a3fab06c89020cc3",
"name": "代码失格"
}
],
"_id": "5dccfc3aa3fab06c89020c65",
"author": "Zander",
"age": 18
}
]

aggregate

FcvdNf

使用 aggregate 实现聚合查询作者 Zander 的基本信息及其所有著作信息:

1
2
3
4
5
6
7
8
9
10
11
12
let result = await Author.aggregate([{ // 操作的Model为Author
$lookup: {
from: "books", // 数据库中关联的集合名
localField: "books", // author文档中关联的字段
foreignField: "_id", // book文档中关联的字段
as: "bookList" // 返回数据的字段名
}
}, {
$match: { // 筛选条件
"author": "Zander"
}
}]);

返回数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[
{
"_id": "5dccfc3aa3fab06c89020c65",
"author": "Zander",
"age": 18,
"bookList": [
{
"_id": "5dccfcb5a3fab06c89020c8d",
"name": "代码的弱点"
},
{
"_id": "5dccfd30a3fab06c89020caa",
"name": "代码与六便士"
},
{
"_id": "5dccfda6a3fab06c89020cc3",
"name": "代码失格"
}
]
}
]

对比

1. 灵活性

现在可以观察到的就是 aggregate 灵活的点在于可以更改关联查询后返回数据的 key(返回数据中的bookList),而 populate 返回数据的 key 只能是原来的字段名(返回数据中的books)。值得一提的是 aggregate 更擅长在聚合管道中对数据进行二次处理,比如$unwind拆分、$group分组等等。

2. 功能性

此外,还有一种情况:依旧是上面的数据,如果要根据著作 name 找到著作信息和作者信息,使用 aggregate 的$lookup只需要这样就做到了😏:

1
2
3
4
5
6
$lookup: {
from: "authors",
localField: "_id",
foreignField: "books",
as: "author"
}

然而 populate:“我太难了!” 是的,它做不到这种使用_id实现的反向关联查询.

3. 性能方面

看完了外表再说说内在——查询性能,populate 实际是DBRef[^4]的引用方式,相当于多构造了一层查询。比如有10条数据,在find()查询到了主集合内的10条数据后会再进行populate()引用的额外10条数据的查询,性能也相对的大打折扣了。这里有位大佬对aggregate()find()进行了性能上的对比,结论也显而易见——比 find 查询速度都快的 aggregate 比关联查询的 find + populate 定是有过之而无不及了。

总结

aggregation populate
灵活性 ⭐️⭐️⭐️⭐️⭐ ⭐️
反向关联 ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️
功能性 ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️
代码简洁度 ⭐️ ⭐️⭐️⭐️⭐️⭐️
查询性能 ⭐️⭐️⭐️⭐️ ⭐️⭐️

综合来看,aggregate 在多集合关联查询和对查询数据的二次处理方面更优,而 populate 更适合简单的正向关联关系且其形成的代码样式较优雅,可读性高而易于维护,性能方面的考究对日常开发中的普通应用来说则大可忽略不计。

关联技巧一:关联引用子查父

FcvdNf

1
2
3
4
5
6
$lookup: {
from: "authors",
localField: "_id",
foreignField: "books",
as: "author"
}

也可通过 mongoose4.5之virtual虚拟值填充 快速获取父内容

关联技巧二:关联数组转对象

1
2
3
4
5
6
7
8
const lookup = {
$lookup: {
from: 'sys_invoices',
localField: 'invoice',
foreignField: '_id',
as: 'invoice',
},
}
1
2
3
4
5
const project = {
$project: {
invoice: { $arrayElemAt: ['$invoice', 0] },
},
}