5 Node 中使用数据
5 数据库
我们在使用 node 处理业务逻辑的时候难免要和数据打交道,这时候数据库就派上用场了。在 node 中我们最常用的数据库有两种,redis 和 mongodb。本章也正是围绕这两个数据库展开讲解。
5.1 redis
redis 提供 key-value 类型的存储结构,是一种内存数据库,因此数据查询速度特别快,而且它还可以通过配置来实现将数据定期备份到磁盘上的功能,一定程度上解决进程掉线后数据恢复的问题。
node 中推荐使用 ioredis 这个驱动来对 redis 进行操作。redis 这个驱动虽然使用人数更多,但是从 redis 3.x 开始增加了 cluster 模式,但是这个驱动并不支持这种模式,所以不推荐使用。
var Redis = require('ioredis');
/**
* 如果不传参数默认连接127.0.0.1:6379端口
* */
var redis = new Redis(/*{"port" : 6379,"host" : "127.0.0.1",password: 'auth'}*/);//没有密码不需要传password参数
/*
var clusterRedis = [
{
"host":"127.0.0.1",
"port":6379
},
{
"host":"127.0.0.1",
"port":6380
}
];
var redis = new Redis.Cluster(clusterRedis,{redisOptions:{password: 'auth'});//集群连接方式
*/
redis.set('foo', 'bar', function(err,reply) {
console.log(err, reply);//正常情况打印 null 'OK'
});
redis.get('foo', function (err, result) {
console.log(err,result);//正常情况打印 null 'bar'
});
代码 5.1.1 redis命令基本演示
redis 中大多数的命令格式都是这样的 command key param1 prama2 ... 对应 ioredis 中的函数就是 redis.command(key, param1, param2, ...) 比如说 代码5.1.1 中的栗子,我们在 redis-cli 中执行 set foo bar 命令就对应我们的 redis.set('foo', 'bar') 这行代码。注意到我们这里在接收处理结果的时候都是使用 callback 的方式,ioredis 内部也支持 promise 方式来接收处理结构,你只需要将回调函数去掉,改成 then 函数:
redis.set('foo','bar').then(function(reply) {
});
代码 5.1.2 使用 promise 方式接收返回数据
有时候我们在使用 redis 的时候,在一个处理逻辑中要连续发送多条 redis 命令,这时候你可以考虑用 ioredis 中提供的 pipeline 或者 multi 函数。
使用 pipeline 时 ioredis 内部将一系列指令缓存到内存,最后通过 exec 函数执行后打包发送到 redis 服务器,而且它支持链式的调用方式:
redis.pipeline().set('foo', 'bar').get('foo').exec(function (err, results) {
});
代码 5.1.3 pipeline 链式调用
甚至可以在调用每个命令的时候都加一个回调函数,这里在get位置加一个回调函数:
redis.pipeline().set('foo', 'bar').get('foo',function(err,result) {
console.log('get foo',err,result);
}).exec(function (err, results) {
console.log('with single callback',err, results);
});
代码 5.1.4 pipeline 链式函数中加回调
当然这里还有一种更加简洁的调用方式,就是都把参数放到数组里:
redis.pipeline([
['set','foo','bar'],
['get','foo']
]).exec(function(err,results) {
console.log('array params',err,results);
});
代码 5.1.5 pipeline 数组参数调用方式
multi 函数跟 pipeline 函数的区别是,multi 提供了事务的功能,提交到 redis 服务器的命令的会被依次执行,pipeline 则是批量执行一批提交一批指令,但是在 redis 内部都是独立执行的,没有先后顺序,只是最终服务器将所有处理结果一起返回给了调用者。不过要想完全保证事务的原子性,我们还需要使用 watch 函数,防止我们在事务中操作一个事务的过程中,当前操作的某一个键值又被其他连接的客户端给修改了:
redis.watch('foo');
redis.multi().set('foo', 'bar').get('foo').exec(function (err, results) {
redis.unwatch();
console.log('chain',err, results);
});
代码 5.1.6 multi 事务操作代码
最后一件需要重点指明的事情是,如果你当前使用了 cluster 方式连接 redis,那么最好不要使用 pipeline 和 multi 因为,ioredis 在调用这两个函数的时候,仅仅会往一个节点发送指令,但是你又不能保证你这里面操作的所有键值都在一个节点上,所以说调用这两个函数的时候很有可能会失败。
5.2 mongodb
mongodb官方提供了 Node.js 的 mongodb 驱动,不过鉴于其提供驱动的功能太过于简单,所以又涌现了许多基于官方驱动上开发的第三方驱动。下面要讲两个使用广泛的第三方驱动,mongoskin 和 mongoose 。
5.2.1 mongoskin
我们这里先讲 mongoskin,在介绍之前先讲清楚一个概念,传统关系型数据库中,有表的概念,mongodb有collection的概念,其实是同一种东西,我在这里仍然称呼collection为表。
为了演示它的用法,我们先不在 express 中使用它,而是写个简单的的测试函数。
var mongo = require('mongoskin');
var db = mongo.db("mongodb://localhost:27017/live", {native_parser:false});
db.bind('article');
代码 5.2.1.1 初始化mongoskin
在代码 5.2.1.1 中,可以看出我们创建了一个mongo连接,服务器服务器地址为localhost,端口为80,参数 native_parser 代表是否使用原生代码来解析 mongodb 的 bson 数据。如果开启这个选项,需要在安装 mongoskin 模块的时候可以编译原生代码,如果你的开发环境是 Windows,且没有安装臃肿的Visual Studio的话,是没法编译原生代码的,那么这个参数就设置为 false。
注意最后一句 db.bind('article') 函数bind返回一个Collection对象,它在db对象上绑定一个article属性,指向刚才返回的对象,这句话在 mongoskin 中等同于 db.article = db.collection('article'); ( collection其实为原生mongodb驱动里面获取一个Collection的封装函数),调用完 bind 函数后我们就可以通过db.article来操作一系列的增删改查了。
首先是插入单条数据:
db.article.insert({
name:'chapter5',content:'Express.js 基础',createTime:new Date('2016/07/03')
},function(err,ret) {
console.log('单条插入',err,ret);
});
代码 5.2.1.2 mongoskin插入单条数据 接着是插入多条数据,仅仅把第一个参数改成数组就可以了:
db.article.insert([
{name:'chapter1',content:'Node.js 简介',createTime:new Date('2016/07/01')},
{name:'chapter2',content:'Node.js 基础',createTime:new Date('2016/07/02')}
],function(err,ret) {
console.log('插入数组',err,ret);
});
代码 5.2.1.3 mongoskin插入多条数据 修改单条数据:
db.article.update({name:'chapter2'},{
$set:{content:'Node.js 入门'}
},function(err,ret) {
console.log('更新单条数据',err,ret);
});
代码 5.2.1.4 mogonskin修改单条数据
注意 第二个参数中需要有一个$set属性,否则整条数据将会被替换掉,如果直接将第二个参数写成了{content:'Node.js 入门'},则操作完成之后,数据库里当前记录就变成了{_id:主键值,content:'Node.js 入门'},之前的属性name和createTime就都丢失了。
代码 5.2.1.5 mongoskin修改单条数据
如果想修改多条数据,只需要增加一个参数:
db.article.update({name:'chapter2'},{
$set:{content:'Node.js 入门'}
},{multi:true},function(err,ret) {
console.log('更新单条数据',err,ret);
});
相比较代码 5.2.1.4 这里多了一个参数,{multi:true}告诉数据库服务器,要更新多条数据。
代码 5.2.1.6 mongoskin修改多条数据
删除和更新相反,默认情况下是删除多条记录:
db.article.remove({name:'chapter1'},function(err,ret) {
console.log('删除数据',err,ret);
});
代码 5.2.1.7 mongoskin删除多条记录
如果想删除一条记录,则增加一个参数 {justone:true},即改成:
db.article.remove({name:'chapter1'},{justone:true},function(err,ret) {
console.log('删除数据',err,ret);
});
代码 5.2.1.8 mongoskin删除单条记录 查询一条记录:
db.article.findOne({name:'chapter2'},function(err,item) {
console.log('查询单条数据',err,item);
});
代码 5.2.1.9 mongoskin查询单体记录
代码 5.2.1.7 中回调函数得到item变量即为查询后得到的记录。
查询多条记录:
db.article.findItems({},function(err, items) {
console.log('查询多条数据',err,items);
});
代码 5.2.1.10 mongoskin查询多条记录 上面演练了一遍mongoskin的增删改查,不过,我们将其和传统的关系型数据库做对比,发现还少了点东西。比如说:一条记录有多个字段,我只想返回若干字段怎么弄;表中的数据过多,想分页显示怎么弄;对于需要使用事务的情形,怎么弄。 对于前两个问题,只需要在查询的时候加参数即可。比如我们想查询7月1日都有哪些文章发布,我们只关心文章名称,同时由于数据量很大,无法全部显示出来,需要做分页,那么查询语句可以这么写:
db.article.findItems({
createTime:{$gte:new Date(2016,6,1),$lt:new Date(2016,6,2)}
},{
fields:{name:1,createTime:1},skip:1,limit:1,sort:{createTime:-1}
},function(err, items) {
console.log('查询多条数据',err,items);
});
代码 5.2.1.11 mongoskin自定义查询选项
在这里我们通过fields来控制返回字段名,skip:1代表跳过第一条记录,limit:1代表从跳过的记录后面取1条记录,同时我们还增加了
sort属性,按照createTime字段的倒序排列。
对于最后一个问题,很遗憾,mongodb中确实没有事务,不过它还是提供了一个带有锁功能的操作函数,就是 findAndModify。这个函数的参数比较多,
下面直接给出参数列表:
函数声明
findAndModify(query, sort, update , options , callback)
参数声明
-
query{Object}查询条件
-
sort{Array}排序
-
update{Object}更新的内容
-
option
{
new:{Boolean} 是否返回更新后的内容,默认为false
upsert: {Boolean} 不存在时是否插入,默认为false
remove : {Boolean} 查询到结果后将其删除,此字段优先级高于 upsert,默认为false
fields:{Object} 指定查询返回结果中要返回的字符,默认为null
}
-
callback{Function} 此回调函数有两个参数:
-
error {Error} 错误对象,成功时为null
-
result {Object}
-
{
value : {Object} 函数findAndModify返回的数据记录
lastErrorObject : { updatedExisting: true, n: 1, connectionId: 14, err: null, ok: 1 } 大体格式是这样的
ok : {Number} 成功返回1
}
举个例子,现在有一张表comment,它的数据结构是这样的:
{
"_id" : ObjectId("5792f2db03d07723cff9ab35"),
"author" : "1vglr42hwqb",
"content" : "twicgcbk7xd",
"articleId" : ObjectId("5792288c0c0c422b282f2f93"),
"createTime" : 1468304429853.0000000000000000
}
现在我们将使用函数findAndModify查询文章ID为ObjectId("5792288c0c0c422b282f2f93")最近的一条评论,将其content字段改成评论已被删除:
db.comment.findAndModify({
articleId:mongo.helper.toObjectID("5792288c0c0c422b282f2f93")
},{'createTime':-1},{$set:{content:'评论已被删除'}},{
fields:{author:1,content:1},new:true,upsert:false,remove:false
},function(err, result) {
console.log('修改后数据',err,result);
});
代码 5.2.1.12 函数findAndModify演示
但是要注意,findAndModify 会同时持有读写锁,也就是在这个函数操作过程中,其他命令是会被堵塞住,一直等待这个函数操作完成或者出错,其他命令才会有机会接着执行。所以一般情况下,不要使用这个函数,除非是非常关键的数据,要求精度很高才使用这个函数,比如说订单状态修改之类的操作,但是就像代码 5.2.1.10之中的操作,纯属拿大炮打蚊子了。即使非要用到这个函数的情况,也尽量保证查询的时候可以使用索引,尽量减少锁持有的时间。
对于一些非关键性数据,但是又必须保证某个字段在写入的时候保持唯一性,那么在这个字段上增加一个唯一索引,就可以了。在mongodb的命令行中执行如下命令:
db.collectionName.ensureIndex({fieldName:1},{unique:true});
命令 5.2.1.1 创建唯一索引
之前已经提过,mongoskin只不过是对与原生mongodb node驱动的封装,其基于
collection的操作函数的各个参数都是通用的,另一方面mongoskin官方给出的API文档并不详尽,所以如果要想了解详细的各个操作函数的说明,可以参考mongodb node驱动的官方文档。
5.2.2 mongoose
前面讲了 mongskin ,算是 mongodb 知识点的开胃菜, mongoskin 中的函数绝大部分和 mongodb 命令行是类似的。下面要讲的 mongoose 却稍有不同,因为其有一个 ODM (O bject D ata M odel) 的概念,类似于 hibernate 开发中用到的 ORM (O bject R elational M apping) 的概念,它提供了一种将 mongodb 中字段映射为 JavaScript 对象属性的能力。如果我们用 mongoose 来实现一系列的增删改查操作,就必须先定义一个 Schema,不过下面要先讲怎样在 mongoose 中建立连接,否则接下来的例子就没法运行了:
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/live', {/*user:'username',pass:'password'*/}); // connect to database
代码 5.2.2.1 mongoose 建立连接代码
在 mongoose 中使用 connect 函数可以初始化 mongodb 连接,第一个参数代表 mongodb 的连接字符串,第二个参数存放连接控制参数,比如说用户名、密码之类的。其实第一个字符串中有更多连接参数控制,可以参考 mongodb 的 官方文档,其中就包括用户名和密码信息(格式为mongodb://username:password[@host](https://github.com/host) "@host"):port/database?options...),但是如果你的密码中有特殊字符的话(比如说@),就比较难办了,所以将用户名和密码放到第二个参数中比较保险。
接下来就将mongoose中非常之重要的 Schema,首先直接构造一个我们在 5.2.1 小节中使用过的 article 的schema 声明:
var mongoose = require('mongoose');
require('./conn');//代码5.2.2.1对应的代码
var Schema = mongoose.Schema;
var articleSchema = new Schema({
name: String,
content: String,
comments: [{ body: String, date: Date }],
create_at: { type: Date, default: Date.now }
});
var Article = mongoose.model('article', articleSchema);
代码 5.2.2.2 声明 Schema
通过以上代码可以总结出 shema 干的事情就是把数据库的各个字段的数据类型定义出来,最后我们还通过 model 函数获得了一个 mongoose 中的 Model 类,mongoose 的增删改查都通过这个类来进行。注意第一个参数代表表名。
new Article({
name:'chapter5',
content:'Express.js 基础',
comments : [
{body:'写的不多',date:new Date('2016-10-11')},
{body:'我顶',date:new Date('2017-01-01')}
],
create_at:new Date('2016/07/03')
}).save(function(err,item) {
console.log(err,item);
});
代码 5.2.2.3 mongoose 插入操作
为啥说 model 函数得到的是类呢,通过 代码 5.2.2.3 就可以看出,我们通过 new 生成一个对象实例,然后调用其 save 函数将其插入数据库。如果我们将 create_at 属性去掉,那么其值就会自动取当前时间。不过等你执行完上述代码后,查看数据库,咦,surprise,数据库里竟然多了一个名字叫 articles 的表,不是说 model 的第一个参数是执行关联的表明吗,明明在 代码 5.2.2.2 中指定的表明是 article 啊?是的,不要惊讶,mongoose 默认就是这么设计的,如果你想绑定到一个自定义的一个表明上,可以在实例化 Schema 的时候,传入一个可选参数:
var articleSchema = new Schema({/*此处省略字段定义*/},{collection:'article'});
这样将 articleShema 插入model 后得到的 Article 就绑定表 article 上了。
说了插入单条,再说一下批量插入,这时候使用 insertMany 函数即可:
Article.insertMany([
{name:'chapter1',content:'Node.js 简介1',create_at:new Date('2016/07/01')},
{name:'chapter1',content:'Node.js 简介2',create_at:new Date('2016/07/01')},
{name:'chapter1',content:'Node.js 简介3',create_at:new Date('2016/07/01')},
{name:'chapter2',content:'Node.js 基础4',create_at:new Date('2016/07/02')},
{name:'chapter2',content:'Node.js 基础5',create_at:new Date('2016/07/02')}
],function(err,ret) {
console.log('插入数组',err,ret);
});
代码 5.2.2.4 mongoose 批量插入操作
mongoose 的修改操作和官方 API 差不多:
Article.update({name:'chapter2'},{
$set:{content:'Node.js 入门'}
},function(err,ret) {
console.log('更新单条数据',err,ret);
});
Article.update({name:'chapter2'},{
$set:{content:'Node.js 入门'}
},{multi:true},function(err,ret) {
console.log('更新多条数据',err,ret);
});
代码 5.2.2.5 mongoose 修改操作
不过它的删除稍微有些不同,就是删除的时候仅仅只能指定一个查询参数,如果你想仅仅删除一条的话,那就需要先查询出来,然后再删除。
Article.findOne({name:'chapter1'}).remove().exec(function(err,ret) {
console.log('删除数据',err,ret);
});
Article.remove({name:'chapter1'},function(err,ret) {
console.log('删除数据',err,ret);
});
代码 5.2.2.6 mongoose 删除操作
上面总结了一下 mongoose 的一些基本用法,不过前面的描述还不足以体现 mongoose 的强大,下面讲到的一些高级用法,绝对能让你感到惊艳。
首先 mongoose 提供了中间件( middleware )的功能,我们可以在执行数据命令前和执行后添加钩子函数,先上代码:
var mongoose = require('mongoose');
require('./conn');//代码6.2.2.1对应的代码
var Schema = mongoose.Schema;
var articleSchema = new Schema({
name: String,
content: String,
comments: [{ body: String, date: Date }],
create_at: { type: Date, default: Date.now }
});
articleSchema.pre('save',function(next) {
this.content = this.name + '\n' + this.content;
next();
});
articleSchema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
var Article = mongoose.model('article', articleSchema);
new Article({
name:'chapter5',
content:'Node 中使用数据库',
comments : [
{body:'写的不多',date:new Date('2016-10-11')},
{body:'我顶',date:new Date('2017-01-01')}
],
create_at:'2017-02-11'
}).save(function(err,item) {
console.log(err,item);
});
代码 5.2.2.7 save 的中间件函数演示
我们创建了一个 article 的 schema 定义,同时定义了两个中间件。通过 pre('save') 操作,我们在文章的第一行拼接了文章的标题,然后注意一定要调用 next 函数,否则当前数据库操作就不会得到执行。通过 post('save') 操作用来在数据库操作完成之后执行一些级联操作,这里我们简单的打印了一下日志。这两个中间件函数会先于 save 函数的回调函数前执行。
在调用 save 函数时,mongoose 中还提供了一个 validate 中间件,他会在 pre(‘save’) 之前被触发,用来校验传入 save 函数的各个属性是不是合法:
articleSchema.pre('validate',function(next) {
if (/<script>/.test(this.content)) {
return next(new Error('文章内容非法'));
}
next();
});
new Article({
name:'chapter5',
content:'Node 中使用数据库<script>alert(document.cookie)</script>',
}).save(function(err,item) {
console.log(err,item);
});
代码 5.2.2.8 save 的 validate 中间件函数演示
上面的代码执行后,会抛出异常,因为我们的 article content 字段中包含 script 标签。令人欣喜的是,mongoose 还提供将 validate 中间件直接加到 schema 定义上的功能:
var mongoose = require('mongoose');
require('./conn');//代码6.2.2.1对应的代码
var Schema = mongoose.Schema;
var articleSchema = new Schema({
name: {
type:String,
required: [true,'必须提供文章标题'],
maxlength : [50,'文章标题不能多于50个字符']
},
isbn : {
type:String,
unique:true,
sparse: true
},
content: {
type:String,
validate:{
validator : function() {
return !(/<script>/.test(this.content));
},
message : '文章内容非法'
}
},
starts : {
type:Number,
min:0,
max:[5,'最多只能给5颗星'],
default:0
},
level : {
type:String,
enum:['专家推荐','潜力无限','家有作家初长成','我只是个小学生']
},
category : {
type:String,
enum:{
values:['诗歌','散文','杂文','议论文','小说'],
message:'当前标签不支持'
}
},
cover_url : {
type:String,
match:[/^http(s?):\/\//,'封面图格式非法']
},
comments: [{ body: String, date: Date }],
create_at: { type: Date, default: Date.now }
});
articleSchema.pre('save',function(next) {
this.content = this.name + '\n' + this.content;
next();
});
articleSchema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
var Article = mongoose.model('article', articleSchema);
new Article({
name:'chapter5',
content:'Node 中使用数据库<script>alert(document.cookie)</script>',
}).save(function(err,item) {
if (err && err.name === 'ValidationError') {
for (var field in err.errors) {
var error = err.errors[field];
console.error(error.message,error.path,error.value);
}
}
});
代码 5.2.2.9 在 schema 中使用校验器
mongoose 内建了好多校验器(validator),多余所有类型字段来说都可以使用 required 校验器,对于 Number 类型字段来说,可以使用 min 和 max 校验器,对于 String 类型字段来说,可以使用 enum match maxlength minlength 校验器。
所有校验器都可以设置在校验失败后的错误提示信息,如果相对某一个字段设置 required 约束,那么可以写成 required:true ,还可以进一步指定校验失败后的提示信息,也就是写成这样 requried:[true,'这个字段必须指定'] 。但是对于 enum 来说,由于本身定义的时候就是一个数组结构(参见上面代码中 level 字段的定义),所以 mongoose 内部在定义其 message 属性时使用这样一个 Object 结构:{values:[/*枚举字段定义*/],message:'出错提示信息'} 。
还记得在代码 5.2.2.8 中我们自定义的那个 content 字段的校验中间件不?这个中间件可以直接写到 schema 定义中,在代码 5.2.2.9 中的 content 字段中的 validate 属性,就能替换掉之前我们写过的校验中间件。
最终你在调用 save 函数之前,这层层的字段定义约束都会被执行,如果校验出错,那么 save 回调函数返回的第一个参数中的 name 属性的值将是 ValidationError,让你后其 errors 属性中保存着字段的详细信息的一个 key-value数据结构,键名是出错的字段名,值是一个包含错误详情的对象,这个对象中 message 属性就是我们在 schema 中设置的出错信息, path 是出错的字段名,value 是引起出错的具体的设置的值。
最终需要注意,unique 这个约束并不是一个 ValidationError (实际上其 name 属性值为 MongoError),所以你 save 失败后得到的error 对象中没有errors 属性。unique 和 sparse 仅仅是 schema 调用 mongodb 的驱动创建了数据库索引而已。代码 5.2.2.9 中关于 isbn 的约束,也可以通过 schema 中的 index 函数来实现:
articleSchema.index('isbn',{unique:true,sparse:true});
代码 5.2.2.10
前面讲了许多 mongoose 的插入、修改之类的操作,一直没有提到查询操作,下面就来讲一下查询。
在讲查询之前,需要先将我们在代码5.2.2.9中定义的 articleSchema 进行一下扩充,增加下面这个字段:
_author : {type:Schema.Types.ObjectId,ref:'user'},
代码 5.2.2.11
至于其中的 _ref 属性是怎么回事,我们先买个关子,一会儿再说。
mongoose 在查询方面,有好多细节做了优化,比如说在筛选返回字段的时候可以直接通过字符串来指定:
Article.findOne({name:nameRand},'name -_id',function(err,item) {
if (err) {
return console.error('findOne',err);
}
console.log('findOne',item && item.name === nameRand);
});
代码 5.2.2.12 mongoose 查询使用字符串筛选字段
mongoose 的查询中的各个控制参数都可以链式的调用各个函数来解决,比如说上例中用到的字段筛选可以使用 select 函数来替代,即改成 Article.findOne({name:nameRand}).select('name -_id').exec(function(err,item) {}); 当中可以添加无数个链式函数来控制查询行为,比如说 limit skip lean 等等,最后以 exec 函数结尾添加回调函数。mongoose 查询默认返回的是 MongooseDocuments 类型对象,使用lean 函数后可以将其转成普通 javascript 对象:
Article.find({name:/^name/}).select('_author').lean().exec(function(err,items) {
if (err) {
return console.error('find',err);
}
console.log('find',items);
});
代码 5.2.2.13 mongoose 查询返回纯 javascript 对象
转纯 javascript 对象的使用场景一般比较少见,当我们拿查询的结果作为参数来调用一些第三方库(比如说 protobufjs )时,不调用lean的情况下会出错。
最后还要暴一下 mongoose 中的大杀器,就是联合查询,其实 mongdb 本身是没有联合查询功能的,这个功能是在 mongoose 层面延伸的功能:
Article
.findById(articleId)
.select('name _author')
.populate('_author','nickname -_id')
.exec(function(err,item) {
if (err) {
return console.error('findById',err);
}
console.log('populate',item);
});
代码 5.2.2.14 mongoose 联合查询功能
还记得我们在代码 5.2.2.10 中卖的关子不,我们看到其中有一个 _ref 属性,它的作用就是告诉 mongoose _author 字段的值对应 users 表中的主键字段,如果在查询的时候使用 populate 函数,则 mongoose 将在底层做两次查询(查询 articles 表 和 users 表),然后把查询结果合并。最终得到的结构演示如下:
{ name: 'name0.6169953700982793',
_author: { nickname: 'nick0.09724390163323227' },
_id: 5916e9178be9f133b4798002 }
5.3 代码
本章代码参见这里:https://github.com/yunnysunny/nodebook-sample/tree/master/chapter5
