5 数据库

我们在使用 node 处理业务逻辑的时候难免要和数据打交道,这时候数据库就派上用场了。在 node 中我们最常用的数据库有两种,redis 和 mongodb。本章也正是围绕这两个数据库展开讲解。

5.1 redis

redis 提供 key-value 类型的存储结构,是一种内存数据库,因此数据查询速度特别快,而且它还可以通过配置来实现将数据定期备份到磁盘上的功能,一定程度上解决进程掉线后数据恢复的问题。

node 中推荐使用 ioredis 这个驱动来对 redis 进行操作。redis 这个驱动虽然使用人数更多,但是从 redis 3.x 开始增加了 cluster 模式,但是这个驱动并不支持这种模式,所以不推荐使用。

  1. var Redis = require('ioredis');
  2. /**
  3. * 如果不传参数默认连接127.0.0.1:6379端口
  4. * */
  5. var redis = new Redis(/*{"port" : 6379,"host" : "127.0.0.1",password: 'auth'}*/);//没有密码不需要传password参数
  6. /*
  7. var clusterRedis = [
  8. {
  9. "host":"127.0.0.1",
  10. "port":6379
  11. },
  12. {
  13. "host":"127.0.0.1",
  14. "port":6380
  15. }
  16. ];
  17. var redis = new Redis.Cluster(clusterRedis,{redisOptions:{password: 'auth'});//集群连接方式
  18. */
  19. redis.set('foo', 'bar', function(err,reply) {
  20. console.log(err, reply);//正常情况打印 null 'OK'
  21. });
  22. redis.get('foo', function (err, result) {
  23. console.log(err,result);//正常情况打印 null 'bar'
  24. });

代码 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 函数:

  1. redis.set('foo','bar').then(function(reply) {
  2. });

代码 5.1.2 使用 promise 方式接收返回数据

有时候我们在使用 redis 的时候,在一个处理逻辑中要连续发送多条 redis 命令,这时候你可以考虑用 ioredis 中提供的 pipeline 或者 multi 函数。

使用 pipeline 时 ioredis 内部将一系列指令缓存到内存,最后通过 exec 函数执行后打包发送到 redis 服务器,而且它支持链式的调用方式:

  1. redis.pipeline().set('foo', 'bar').get('foo').exec(function (err, results) {
  2. });

代码 5.1.3 pipeline 链式调用

甚至可以在调用每个命令的时候都加一个回调函数,这里在get位置加一个回调函数:

  1. redis.pipeline().set('foo', 'bar').get('foo',function(err,result) {
  2. console.log('get foo',err,result);
  3. }).exec(function (err, results) {
  4. console.log('with single callback',err, results);
  5. });

代码 5.1.4 pipeline 链式函数中加回调

当然这里还有一种更加简洁的调用方式,就是都把参数放到数组里:

  1. redis.pipeline([
  2. ['set','foo','bar'],
  3. ['get','foo']
  4. ]).exec(function(err,results) {
  5. console.log('array params',err,results);
  6. });

代码 5.1.5 pipeline 数组参数调用方式

multi 函数跟 pipeline 函数的区别是,multi 提供了事务的功能,提交到 redis 服务器的命令的会被依次执行,pipeline 则是批量执行一批提交一批指令,但是在 redis 内部都是独立执行的,没有先后顺序,只是最终服务器将所有处理结果一起返回给了调用者。不过要想完全保证事务的原子性,我们还需要使用 watch 函数,防止我们在事务中操作一个事务的过程中,当前操作的某一个键值又被其他连接的客户端给修改了:

  1. redis.watch('foo');
  2. redis.multi().set('foo', 'bar').get('foo').exec(function (err, results) {
  3. redis.unwatch();
  4. console.log('chain',err, results);
  5. });

代码 5.1.6 multi 事务操作代码

最后一件需要重点指明的事情是,如果你当前使用了 cluster 方式连接 redis,那么最好不要使用 pipeline 和 multi 因为,ioredis 在调用这两个函数的时候,仅仅会往一个节点发送指令,但是你又不能保证你这里面操作的所有键值都在一个节点上,所以说调用这两个函数的时候很有可能会失败。

5.2 mongodb

mongodb官方提供了 Node.js 的 mongodb 驱动,不过鉴于其提供驱动的功能太过于简单,所以又涌现了许多基于官方驱动上开发的第三方驱动。下面要讲两个使用广泛的第三方驱动,mongoskinmongoose

5.2.1 mongoskin

我们这里先讲 mongoskin,在介绍之前先讲清楚一个概念,传统关系型数据库中,有表的概念,mongodb有collection的概念,其实是同一种东西,我在这里仍然称呼collection为
为了演示它的用法,我们先不在 express 中使用它,而是写个简单的的测试函数。

  1. var mongo = require('mongoskin');
  2. var db = mongo.db("mongodb://localhost:27017/live", {native_parser:false});
  3. 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来操作一系列的增删改查了。
首先是插入单条数据:

  1. db.article.insert({
  2. name:'chapter5',content:'Express.js 基础',createTime:new Date('2016/07/03')
  3. },function(err,ret) {
  4. console.log('单条插入',err,ret);
  5. });

代码 5.2.1.2 mongoskin插入单条数据
接着是插入多条数据,仅仅把第一个参数改成数组就可以了:

  1. db.article.insert([
  2. {name:'chapter1',content:'Node.js 简介',createTime:new Date('2016/07/01')},
  3. {name:'chapter2',content:'Node.js 基础',createTime:new Date('2016/07/02')}
  4. ],function(err,ret) {
  5. console.log('插入数组',err,ret);
  6. });

代码 5.2.1.3 mongoskin插入多条数据
修改单条数据:

  1. db.article.update({name:'chapter2'},{
  2. $set:{content:'Node.js 入门'}
  3. },function(err,ret) {
  4. console.log('更新单条数据',err,ret);
  5. });

代码 5.2.1.4 mogonskin修改单条数据
注意第二个参数中需要有一个$set属性,否则整条数据将会被替换掉,如果直接将第二个参数写成了{content:'Node.js 入门'},则操作完成之后,数据库里当前记录就变成了{_id:主键值,content:'Node.js 入门'},之前的属性namecreateTime就都丢失了。
代码 5.2.1.5 mongoskin修改单条数据
如果想修改多条数据,只需要增加一个参数:

  1. db.article.update({name:'chapter2'},{
  2. $set:{content:'Node.js 入门'}
  3. },{multi:true},function(err,ret) {
  4. console.log('更新单条数据',err,ret);
  5. });

相比较代码 5.2.1.4 这里多了一个参数,{multi:true}告诉数据库服务器,要更新多条数据。
代码 5.2.1.6 mongoskin修改多条数据
删除和更新相反,默认情况下是删除多条记录:

  1. db.article.remove({name:'chapter1'},function(err,ret) {
  2. console.log('删除数据',err,ret);
  3. });

代码 5.2.1.7 mongoskin删除多条记录
如果想删除一条记录,则增加一个参数 {justone:true},即改成:

  1. db.article.remove({name:'chapter1'},{justone:true},function(err,ret) {
  2. console.log('删除数据',err,ret);
  3. });

代码 5.2.1.8 mongoskin删除单条记录
查询一条记录:

  1. db.article.findOne({name:'chapter2'},function(err,item) {
  2. console.log('查询单条数据',err,item);
  3. });

代码 5.2.1.9 mongoskin查询单体记录
代码 5.2.1.7 中回调函数得到item变量即为查询后得到的记录。
查询多条记录:

  1. db.article.findItems({},function(err, items) {
  2. console.log('查询多条数据',err,items);
  3. });

代码 5.2.1.10 mongoskin查询多条记录
上面演练了一遍mongoskin的增删改查,不过,我们将其和传统的关系型数据库做对比,发现还少了点东西。比如说:一条记录有多个字段,我只想返回若干字段怎么弄;表中的数据过多,想分页显示怎么弄;对于需要使用事务的情形,怎么弄。
对于前两个问题,只需要在查询的时候加参数即可。比如我们想查询7月1日都有哪些文章发布,我们只关心文章名称,同时由于数据量很大,无法全部显示出来,需要做分页,那么查询语句可以这么写:

  1. db.article.findItems({
  2. createTime:{$gte:new Date(2016,6,1),$lt:new Date(2016,6,2)}
  3. },{
  4. fields:{name:1,createTime:1},skip:1,limit:1,sort:{createTime:-1}
  5. },function(err, items) {
  6. console.log('查询多条数据',err,items);
  7. });

代码 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

    1. {
    2. new:{Boolean} 是否返回更新后的内容,默认为false
    3. upsert: {Boolean} 不存在时是否插入,默认为false
    4. remove : {Boolean} 查询到结果后将其删除,此字段优先级高于 upsert,默认为false
    5. fields:{Object} 指定查询返回结果中要返回的字符,默认为null
    6. }
  • callback{Function} 此回调函数有两个参数:

    • error {Error} 错误对象,成功时为null

    • result {Object}

      1. {
      2. value : {Object} 函数findAndModify返回的数据记录
      3. lastErrorObject : { updatedExisting: true, n: 1, connectionId: 14, err: null, ok: 1 } 大体格式是这样的
      4. ok {Number} 成功返回1
      5. }

举个例子,现在有一张表comment,它的数据结构是这样的:

  1. {
  2. "_id" : ObjectId("5792f2db03d07723cff9ab35"),
  3. "author" : "1vglr42hwqb",
  4. "content" : "twicgcbk7xd",
  5. "articleId" : ObjectId("5792288c0c0c422b282f2f93"),
  6. "createTime" : 1468304429853.0000000000000000
  7. }

现在我们将使用函数findAndModify查询文章ID为ObjectId("5792288c0c0c422b282f2f93")最近的一条评论,将其content字段改成评论已被删除:

  1. db.comment.findAndModify({
  2. articleId:mongo.helper.toObjectID("5792288c0c0c422b282f2f93")
  3. },{'createTime':-1},{$set:{content:'评论已被删除'}},{
  4. fields:{author:1,content:1},new:true,upsert:false,remove:false
  5. },function(err, result) {
  6. console.log('修改后数据',err,result);
  7. });

代码 5.2.1.12 函数findAndModify演示
但是要注意,findAndModify 会同时持有读写锁,也就是在这个函数操作过程中,其他命令是会被堵塞住,一直等待这个函数操作完成或者出错,其他命令才会有机会接着执行。所以一般情况下,不要使用这个函数,除非是非常关键的数据,要求精度很高才使用这个函数,比如说订单状态修改之类的操作,但是就像代码 5.2.1.10之中的操作,纯属拿大炮打蚊子了。即使非要用到这个函数的情况,也尽量保证查询的时候可以使用索引,尽量减少锁持有的时间。
对于一些非关键性数据,但是又必须保证某个字段在写入的时候保持唯一性,那么在这个字段上增加一个唯一索引,就可以了。在mongodb的命令行中执行如下命令:

  1. 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 (Object Data Model) 的概念,类似于 hibernate 开发中用到的 ORM (Object Relational Mapping) 的概念,它提供了一种将 mongodb 中字段映射为 JavaScript 对象属性的能力。如果我们用 mongoose 来实现一系列的增删改查操作,就必须先定义一个 Schema,不过下面要先讲怎样在 mongoose 中建立连接,否则接下来的例子就没法运行了:

  1. var mongoose = require('mongoose');
  2. 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:port/database?options...),但是如果你的密码中有特殊字符的话(比如说@),就比较难办了,所以将用户名和密码放到第二个参数中比较保险。

接下来就将mongoose中非常之重要的 Schema,首先直接构造一个我们在 5.2.1 小节中使用过的 article 的schema 声明:

  1. var mongoose = require('mongoose');
  2. require('./conn');//代码5.2.2.1对应的代码
  3. var Schema = mongoose.Schema;
  4. var articleSchema = new Schema({
  5. name: String,
  6. content: String,
  7. comments: [{ body: String, date: Date }],
  8. create_at: { type: Date, default: Date.now }
  9. });
  10. var Article = mongoose.model('article', articleSchema);

代码 5.2.2.2 声明 Schema

通过以上代码可以总结出 shema 干的事情就是把数据库的各个字段的数据类型定义出来,最后我们还通过 model 函数获得了一个 mongoose 中的 Model 类,mongoose 的增删改查都通过这个类来进行。注意第一个参数代表表名。

  1. new Article({
  2. name:'chapter5',
  3. content:'Express.js 基础',
  4. comments : [
  5. {body:'写的不多',date:new Date('2016-10-11')},
  6. {body:'我顶',date:new Date('2017-01-01')}
  7. ],
  8. create_at:new Date('2016/07/03')
  9. }).save(function(err,item) {
  10. console.log(err,item);
  11. });

代码 5.2.2.3 mongoose 插入操作

为啥说 model 函数得到的是类呢,通过 代码 5.2.2.3 就可以看出,我们通过 new 生成一个对象实例,然后调用其 save 函数将其插入数据库。如果我们将 create_at 属性去掉,那么其值就会自动取当前时间。不过等你执行完上述代码后,查看数据库,咦,surprise,数据库里竟然多了一个名字叫 articles 的表,不是说 model 的第一个参数是执行关联的表明吗,明明在 代码 5.2.2.2 中指定的表明是 article 啊?是的,不要惊讶,mongoose 默认就是这么设计的,如果你想绑定到一个自定义的一个表明上,可以在实例化 Schema 的时候,传入一个可选参数:

  1. var articleSchema = new Schema({/*此处省略字段定义*/},{collection:'article'});

这样将 articleShema 插入model 后得到的 Article 就绑定表 article 上了。

说了插入单条,再说一下批量插入,这时候使用 insertMany 函数即可:

  1. Article.insertMany([
  2. {name:'chapter1',content:'Node.js 简介1',create_at:new Date('2016/07/01')},
  3. {name:'chapter1',content:'Node.js 简介2',create_at:new Date('2016/07/01')},
  4. {name:'chapter1',content:'Node.js 简介3',create_at:new Date('2016/07/01')},
  5. {name:'chapter2',content:'Node.js 基础4',create_at:new Date('2016/07/02')},
  6. {name:'chapter2',content:'Node.js 基础5',create_at:new Date('2016/07/02')}
  7. ],function(err,ret) {
  8. console.log('插入数组',err,ret);
  9. });

代码 5.2.2.4 mongoose 批量插入操作

mongoose 的修改操作和官方 API 差不多:

  1. Article.update({name:'chapter2'},{
  2. $set:{content:'Node.js 入门'}
  3. },function(err,ret) {
  4. console.log('更新单条数据',err,ret);
  5. });
  6. Article.update({name:'chapter2'},{
  7. $set:{content:'Node.js 入门'}
  8. },{multi:true},function(err,ret) {
  9. console.log('更新多条数据',err,ret);
  10. });

代码 5.2.2.5 mongoose 修改操作

不过它的删除稍微有些不同,就是删除的时候仅仅只能指定一个查询参数,如果你想仅仅删除一条的话,那就需要先查询出来,然后再删除。

  1. Article.findOne({name:'chapter1'}).remove().exec(function(err,ret) {
  2. console.log('删除数据',err,ret);
  3. });
  4. Article.remove({name:'chapter1'},function(err,ret) {
  5. console.log('删除数据',err,ret);
  6. });

代码 5.2.2.6 mongoose 删除操作

上面总结了一下 mongoose 的一些基本用法,不过前面的描述还不足以体现 mongoose 的强大,下面讲到的一些高级用法,绝对能让你感到惊艳。

首先 mongoose 提供了中间件( middleware )的功能,我们可以在执行数据命令前和执行后添加钩子函数,先上代码:

  1. var mongoose = require('mongoose');
  2. require('./conn');//代码6.2.2.1对应的代码
  3. var Schema = mongoose.Schema;
  4. var articleSchema = new Schema({
  5. name: String,
  6. content: String,
  7. comments: [{ body: String, date: Date }],
  8. create_at: { type: Date, default: Date.now }
  9. });
  10. articleSchema.pre('save',function(next) {
  11. this.content = this.name + '\n' + this.content;
  12. next();
  13. });
  14. articleSchema.post('save', function(doc) {
  15. console.log('%s has been saved', doc._id);
  16. });
  17. var Article = mongoose.model('article', articleSchema);
  18. new Article({
  19. name:'chapter5',
  20. content:'Node 中使用数据库',
  21. comments : [
  22. {body:'写的不多',date:new Date('2016-10-11')},
  23. {body:'我顶',date:new Date('2017-01-01')}
  24. ],
  25. create_at:'2017-02-11'
  26. }).save(function(err,item) {
  27. console.log(err,item);
  28. });

代码 5.2.2.7 save 的中间件函数演示

我们创建了一个 article 的 schema 定义,同时定义了两个中间件。通过 pre('save') 操作,我们在文章的第一行拼接了文章的标题,然后注意一定要调用 next 函数,否则当前数据库操作就不会得到执行。通过 post('save') 操作用来在数据库操作完成之后执行一些级联操作,这里我们简单的打印了一下日志。这两个中间件函数会先于 save 函数的回调函数前执行。

在调用 save 函数时,mongoose 中还提供了一个 validate 中间件,他会在 pre(‘save’) 之前被触发,用来校验传入 save 函数的各个属性是不是合法:

  1. articleSchema.pre('validate',function(next) {
  2. if (/<script>/.test(this.content)) {
  3. return next(new Error('文章内容非法'));
  4. }
  5. next();
  6. });
  7. new Article({
  8. name:'chapter5',
  9. content:'Node 中使用数据库<script>alert(document.cookie)</script>',
  10. }).save(function(err,item) {
  11. console.log(err,item);
  12. });

代码 5.2.2.8 save 的 validate 中间件函数演示

上面的代码执行后,会抛出异常,因为我们的 article content 字段中包含 script 标签。令人欣喜的是,mongoose 还提供将 validate 中间件直接加到 schema 定义上的功能:

  1. var mongoose = require('mongoose');
  2. require('./conn');//代码6.2.2.1对应的代码
  3. var Schema = mongoose.Schema;
  4. var articleSchema = new Schema({
  5. name: {
  6. type:String,
  7. required: [true,'必须提供文章标题'],
  8. maxlength : [50,'文章标题不能多于50个字符']
  9. },
  10. isbn : {
  11. type:String,
  12. unique:true,
  13. sparse: true
  14. },
  15. content: {
  16. type:String,
  17. validate:{
  18. validator : function() {
  19. return !(/<script>/.test(this.content));
  20. },
  21. message : '文章内容非法'
  22. }
  23. },
  24. starts : {
  25. type:Number,
  26. min:0,
  27. max:[5,'最多只能给5颗星'],
  28. default:0
  29. },
  30. level : {
  31. type:String,
  32. enum:['专家推荐','潜力无限','家有作家初长成','我只是个小学生']
  33. },
  34. category : {
  35. type:String,
  36. enum:{
  37. values:['诗歌','散文','杂文','议论文','小说'],
  38. message:'当前标签不支持'
  39. }
  40. },
  41. cover_url : {
  42. type:String,
  43. match:[/^http(s?):\/\//,'封面图格式非法']
  44. },
  45. comments: [{ body: String, date: Date }],
  46. create_at: { type: Date, default: Date.now }
  47. });
  48. articleSchema.pre('save',function(next) {
  49. this.content = this.name + '\n' + this.content;
  50. next();
  51. });
  52. articleSchema.post('save', function(doc) {
  53. console.log('%s has been saved', doc._id);
  54. });
  55. var Article = mongoose.model('article', articleSchema);
  56. new Article({
  57. name:'chapter5',
  58. content:'Node 中使用数据库<script>alert(document.cookie)</script>',
  59. }).save(function(err,item) {
  60. if (err && err.name === 'ValidationError') {
  61. for (var field in err.errors) {
  62. var error = err.errors[field];
  63. console.error(error.message,error.path,error.value);
  64. }
  65. }
  66. });

代码 5.2.2.9 在 schema 中使用校验器

mongoose 内建了好多校验器(validator),多余所有类型字段来说都可以使用 required 校验器,对于 Number 类型字段来说,可以使用 minmax 校验器,对于 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 函数来实现:

  1. articleSchema.index('isbn',{unique:true,sparse:true});

代码 5.2.2.10

前面讲了许多 mongoose 的插入、修改之类的操作,一直没有提到查询操作,下面就来讲一下查询。

在讲查询之前,需要先将我们在代码5.2.2.9中定义的 articleSchema 进行一下扩充,增加下面这个字段:

  1. _author : {type:Schema.Types.ObjectId,ref:'user'},

代码 5.2.2.11

至于其中的 _ref 属性是怎么回事,我们先买个关子,一会儿再说。

mongoose 在查询方面,有好多细节做了优化,比如说在筛选返回字段的时候可以直接通过字符串来指定:

  1. Article.findOne({name:nameRand},'name -_id',function(err,item) {
  2. if (err) {
  3. return console.error('findOne',err);
  4. }
  5. console.log('findOne',item && item.name === nameRand);
  6. });

代码 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 对象:

  1. Article.find({name:/^name/}).select('_author').lean().exec(function(err,items) {
  2. if (err) {
  3. return console.error('find',err);
  4. }
  5. console.log('find',items);
  6. });

代码 5.2.2.13 mongoose 查询返回纯 javascript 对象

转纯 javascript 对象的使用场景一般比较少见,当我们拿查询的结果作为参数来调用一些第三方库(比如说 protobufjs )时,不调用lean的情况下会出错。

最后还要暴一下 mongoose 中的大杀器,就是联合查询,其实 mongdb 本身是没有联合查询功能的,这个功能是在 mongoose 层面延伸的功能:

  1. Article
  2. .findById(articleId)
  3. .select('name _author')
  4. .populate('_author','nickname -_id')
  5. .exec(function(err,item) {
  6. if (err) {
  7. return console.error('findById',err);
  8. }
  9. console.log('populate',item);
  10. });

代码 5.2.2.14 mongoose 联合查询功能

还记得我们在代码 5.2.2.10中卖的关子不,我们看到其中有一个 _ref 属性,它的作用就是告诉 mongoose _author 字段的值对应 users 表中的主键字段,如果在查询的时候使用 populate 函数,则 mongoose 将在底层做两次查询(查询 articles 表 和 users 表),然后把查询结果合并。最终得到的结构演示如下:

  1. { name: 'name0.6169953700982793',
  2. _author: { nickname: 'nick0.09724390163323227' },
  3. _id: 5916e9178be9f133b4798002 }

5.3 代码

本章代码参见这里:https://github.com/yunnysunny/nodebook-sample/tree/master/chapter5