Mongoose 实现关联查询和踩坑记录
本文源自工作中的一个问题,在使用 Mongoose 做关联查询时发现使用 populate() 方法不能直接关联非 _id 之外的其它字段,在网上搜索时这块的解决方案也并不是很多,在经过一番查阅、测试之后,有两种可行的方案,使用 Mongoose 的 virtual 结合 populate 和 MongoDB 原生提供的 Aggregate 里面的 $lookup 阶段来实现。
文档内嵌与引用模式
MongoDB 是一种文档对象模型,使用起来很灵活,它的文档结构分为 内嵌和引用 两种类型。
内嵌是把相关联的数据保存在同一个文档内,我们可以用对象或数组的形式来存储,这样好处是我们可以在一个单一操作内完成,可以发送较少的请求到数据库服务端,但是这种内嵌类型也是一种冗余的数据模型,会造成数据的重复,如果很复杂的一对多或多对多的关系,表达起来就很复杂,也要注意内嵌还有一个最大的单条文档记录限制为 16MB。
引用模型是一种规范化的数据模型,通过主外键的方式来关联多个文档之间的引用关系,减少了数据的冗余,在使用这种数据模型中就要用到关联查询,也就是本文我们要讲解的重点。
图片来源:mongoing[1]
引用模型示例
JSON 模型
我们通过作者和书籍的关系,一个作者对应多个书籍这样一个简单的示例来学习如何在 MongoDB 中实现关联非 _id 查询。
-
Author
{
"bookIds":[
26351021,
26854244,
27620408
],
"authorId":1,
"name":"Kyle Simpson"
}
-
Book
[
{
"bookId":26351021,
"name":"你不知道的JavaScript(上卷)",
},
{
"bookId":26854244,
"name":"你不知道的JavaScript(中卷)",
},
{
"bookId":27620408,
"name":"你不知道的JavaScript(下卷)",
}
]
定义 Schema
使用 Mongoose 第一步要先定义集合的 Schema。
-
author.js
创建 model/author.js 定义作者的 Schema,代码中的 ref 表示要关联的 Model 是谁,在 Schema 定义好之后后面我会创建 Model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const AuthorSchema = new Schema({
authorId: Number,
name: String,
bookIds: [{ type: Number, ref: 'Books' }]
});
AuthorSchema.index({ authorId: 1}, { unique: true });
module.exports = AuthorSchema;
-
book.js
创建 model/book.js 定义书籍的 Schema。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const BookSchema = new Schema({
bookId: Number,
name: String,
});
BookSchema.index({ bookId: 1}, { unique: true });
module.exports = BookSchema;
-
index.js
创建 model/index.js 定义 Model 和链接数据库。
const mongoose = require('mongoose');
const AuthorSchema = require('./author');
const BookSchema = require('./book');
const DB_URL = process.env.DB_URL;
const AuthorModel = mongoose.model('Authors', AuthorSchema, 'authors');
const BookModel = mongoose.model('Books', BookSchema, 'books');
mongoose.set('useCreateIndex', true)
mongoose.connect(DB_URL, {useNewUrlParser: true, useUnifiedTopology: true});
module.exports = {
AuthorModel,
BookModel,
}
使用 Aggregate 的 $lookup 实现关联查询
MongoDB 3.2 版本新增加了 $lookup
实现多表关联,在聚合管道阶段中使用,经过 $lookup
阶段的处理,输出的新文档中会包含一个新生成的数组列。
创建一个 aggregateTest.js 重点在于 $lookup 对象,代码如下所示:
-
$lookup.from: 在同一个数据库中指定要 Join 的集合的名称。 -
$lookup.localFiled: 关联的源集合中的字段,本示例中是 Authors 表的 authorId 字段。 -
$lookup.foreignFiled: 被 Join 的集合的字段,本示例中是 Books 表的 bookId 字段。 -
$as: 别名,关联查询返回的这个结果起一个新的名称。
如果需要指定哪些字段返回,哪些需要过滤,可定义 $project 对象,关联查询的字段过滤可使用 别名.关联文档中的字段 进行指定。
const { AuthorModel } = require('./model');
(async () => {
const res = await AuthorModel.aggregate([
{
$match: { authorId: 1 }
},
{
$lookup: {
from: 'books',
localField: 'bookIds',
foreignField: 'bookId',
as: 'bookList',
}
},
{
$project: {
'_id': 0,
'authorId': 1,
'name': 1,
'bookList.bookId': 1, // 指定 books 表的 bookId 字段返回
'bookList.name': 1
}
}
]);
console.log(JSON.stringify(res));
})();
运行以上程序,将得到以下结果:
[
{
"authorId":1,
"name":"Kyle Simpson",
"bookList":[
{
"bookId":26351021,
"name":"你不知道的JavaScript(上卷)"
},
{
"bookId":26854244,
"name":"你不知道的JavaScript(中卷)"
},
{
"bookId":27620408,
"name":"你不知道的JavaScript(下卷)"
}
]
}
]
关于 $lookup 更多操作参考 MongoDB 官方文档 #lookup-aggregation[2]
Mongoose Virtual 和 populate 实现
Mongoose 的 populate 方法默认情况下是指向的要关联的集合的 _id 字段,并且在 populate 方法里无法更改的,但是在 Mongoose 4.5.0 之后增加了虚拟值填充[3],以便实现文档中更复杂的一些关系。
在我们本节示例中 Authors 集合会关联 Books 集合,那么我们就需要在 Authors 集合中定义 virtual, 下面的一些参数和 $lookup 是一样的,个别参数做下介绍:
-
ref: 表示的要 Join 的集合的名称,同 $lookup.from -
justOne: 默认为 false 返回多条数据,如果设置为 true 就只会返回一条数据
AuthorSchema.virtual('bookList', {
ref: 'Books',
localField: 'bookIds',
foreignField: 'bookId',
justOne: false,
});
之前在这样设置之后,发现没有效果,这里还要注意一点: 虚拟值默认不会被 toJSON() 或 toObject 输出。
如果你需要填充的虚拟值的显示是在 JSON 序列化中输出,就需要设置 toJSON 属性,例如 console.log(JSON.stringify(res))。如果是直接显示的对象,就需要设置 toObject 属性,例如直接打印 console.log(res)。
可以在创建 Schema 时在第二个参数 options 中设置,也可以使用创建的 Schema 对象的 set 方法设置。
const AuthorSchema = new Schema({
authorId: Number,
name: String,
bookIds: [{ type: Number, ref: 'Books' }]
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true },
});
// 或以下方式
// AuthorSchema.set('toObject', { virtuals: true });
// AuthorSchema.set('toJSON', { virtuals: true });
经过以上设置之后就可以使用 populate 做关联查询。
const { AuthorModel } = require('./model');
(async () => {
const res = await AuthorModel.findOne({ authorId: 1 })
.populate({
path: 'bookList',
select: 'bookId name -_id'
});
})();
Mongoose 的虚拟值填充,还可以对匹配的文档数量进行计数,使用如下:
// model/author.js
AuthorSchema.virtual('bookListCount', {
ref: 'Books',
localField: 'bookIds',
foreignField: 'bookId',
count: true
});
// populateTest.js
const res = await AuthorModel.findOne({ authorId: 1 }).populate('bookListCount');
console.log(res.bookListCount); // 3
总结
本文主要是介绍了在 Mongoose 关联查询时如何关联一个非 _id 字段,一种方式是直接使用 MongoDB 原生提供的 Aggregate 聚合管道的 $lookup
阶段来实现,这种方式使用起来灵活,可操作的空间更大,例如通过 as 即可对字段设置别名,还可以使用 $unwind
等关键字对数据做二次处理。另外一种是 Mongoose 提供的 populate 方法,这种方式写起来,代码会更简洁些,这里需要注意如果关联的字段是非 _id 字段,一定要在 Schema 中设置虚拟值填充,否则 populate 关联时会失败。
Github 获取文中代码示例 mongoose-populate[4]
参考资料
mongoing: https://mongoing.com/docs/core/data-modeling-introduction.html#references
[2]#lookup-aggregation: https://docs.mongodb.com/v4.2/reference/operator/aggregation/lookup/index.html
[3]虚拟值填充: http://www.mongoosejs.net/docs/populate.html#populate-virtuals
[4]mongoose-populate: https://github.com/qufei1993/Examples/tree/master/code/database/mongoose-populate
敬请关注「Nodejs技术栈」微信公众号,获取优质文章,如需投稿可在后台留言与我取得联系。
本文分享自微信公众号 - Nodejs技术栈(NodejsRoadmap)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
从点一个灯开始学写Linux字符设备驱动
关注、星标 嵌入式客栈 ,精彩及时送达 [导读] 前一篇文章,介绍了如何将一个hello word模块编译进内核或者编译为动态加载内核模块,本篇来介绍一下如何利用Linux驱动模型来完成一个LED灯设备驱动。点一个灯有什么好谈呢?况且Linux下有专门的leds驱动子系统。 点灯有啥好聊呢? 在很多嵌入式系统里,有可能需要实现数字开关量输出,比如: LED状态显示 阀门/继电器控制 蜂鸣器 ...... 嵌入式Linux一般需求千变万化,也不可能这些需求都有现成设备驱动代码可供使用,所以如何学会完成一个开关量输出设备的驱动,一方面点个灯可以比较快了解如何具体写一个字符类设备驱动,另一方面实际项目中对于开关量输出设备就可以这样干,所以是具有较强的实用价值的。 要完成这样一个开关量输出GPIO的驱动程序,需要梳理梳理下面这些概念: 设备编号 设备挂载 关键数据结构 设备编号 字符设备是通过文件系统内的设备名称进行访问的,其本质是设备文件系统树的节点。故Linux下设备也是一个文件,Linux下字符设备在/dev目录下。可以在开发板的控制台或者编译的主Linux系统中利用ls -l /dev...
- 下一篇
你用对锁了吗?浅谈 Java “锁” 事
每个时代,都不会亏待会学习的人 大家好,我是yes。 本来打算继续写消息队列的东西的,但是最近在带新同事,发现新同事对于锁这方面有一些误解,所以今天就来谈谈“锁”事和 Java 中的并发安全容器使用有哪些注意点。 不过在这之前还是得先来盘一盘为什么需要锁这玩意,这得从并发 BUG 的源头说起。 并发 BUG 的源头 这个问题我 19 年的时候写过一篇文章, 现在回头看那篇文章真的是羞涩啊。 让我们来看下这个源头是什么,我们知道电脑有CPU、内存、硬盘,硬盘的读取速度最慢,其次是内存的读取,内存的读取相对于 CPU 的运行又太慢了,因此又搞了个CPU缓存,L1、L2、L3。 正是这个CPU缓存再加上现在多核CPU的情况产生了并发BUG。 这就一个很简单的代码,如果此时有线程 A 和线程 B 分别在 CPU - A 和 CPU - B 中执行这个方法,它们的操作是先将 a 从主存取到 CPU 各自的缓存中,此时它们缓存中 a 的值都是 0。 然后它们分别执行 a++,此时它们各自眼中 a 的值都是 1,之后把 a 刷到主存的时候 a 的值还是1,这就出现问题了,明明执行了两次加一最终的结果...
相关文章
文章评论
共有0条评论来说两句吧...