github编辑

5 Node 中使用数据

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

5.1 redis

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

node 中推荐使用 ioredisarrow-up-right 这个驱动来对 redis 进行操作。redisarrow-up-right 这个驱动虽然使用人数更多,但是从 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 函数:

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

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

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

代码 5.1.3 pipeline 链式调用

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

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

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

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

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

代码 5.1.6 multi 事务操作代码

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

5.2 mongodb

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

5.2.1 mongoose

前面讲了 mongskin ,算是 mongodb 知识点的开胃菜, mongoskin 中的函数绝大部分和 mongodb 命令行是类似的。下面要讲的 mongoose 却稍有不同,因为其有一个 ODM (Object Data Model) 的概念,类似于 hibernatearrow-up-right 开发中用到的 ORM (Object Relational Mapping)arrow-up-right 的概念,它提供了一种将 mongodb 中字段映射为 JavaScript 对象属性的能力。如果我们用 mongoose 来实现一系列的增删改查操作,就必须先定义一个 Schema,不过下面要先讲怎样在 mongoose 中建立连接,否则接下来的例子就没法运行了:

代码 5.2.1.1 mongoose 建立连接代码

在 mongoose 中使用 connectarrow-up-right 函数可以初始化 mongodb 连接,第一个参数代表 mongodb 的连接字符串,第二个参数存放连接控制参数,比如说用户名、密码之类的。其实第一个字符串中有更多连接参数控制,可以参考 mongodb 的 官方文档arrow-up-right,其中就包括用户名和密码信息(格式为 mongodb://username:password@host:port/database?options...),但是如果你的密码中有特殊字符的话(比如说@),就比较难办了,所以将用户名和密码放到第二个参数中比较保险。

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

代码 5.2.1.2 声明 Schema

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

代码 5.2.1.3 mongoose 插入操作

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

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

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

代码 5.2.1.4 mongoose 批量插入操作

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

代码 5.2.1.5 mongoose 修改操作

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

代码 5.2.1.6 mongoose 删除操作

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

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

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

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

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

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

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

代码 5.2.1.9 在 schema 中使用校验器

mongoose 内建了好多校验器(validator),对于所有类型字段来说都可以使用 requiredarrow-up-right 校验器,对于 Number 类型字段来说,可以使用 minarrow-up-rightmaxarrow-up-right 校验器,对于 String 类型字段来说,可以使用 enumarrow-up-right matcharrow-up-right maxlengtharrow-up-right minlengtharrow-up-right 校验器。

所有校验器都可以设置在校验失败后的错误提示信息,如果相对某一个字段设置 required 约束,那么可以写成 required:true ,还可以进一步指定校验失败后的提示信息,也就是写成这样 required:[true,'这个字段必须指定'] 。但是对于 enum 来说,由于本身定义的时候就是一个数组结构(参见上面代码中 level 字段的定义),所以 mongoose 内部在定义其 message 属性时使用这样一个 Object 结构:{values:[/*枚举字段定义*/],message:'出错提示信息'}

还记得在代码 5.2.1.8中我们自定义的那个 content 字段的校验中间件不?这个中间件可以直接写到 schema 定义中,在代码 5.2.1.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.1.9 中关于 isbn 的约束,也可以通过 schema 中的 indexarrow-up-right 函数来实现:

代码 5.2.1.10

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

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

代码 5.2.1.11

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

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

代码 5.2.1.12 mongoose 查询使用字符串筛选字段

mongoose 的查询中的各个控制参数都可以链式的调用各个函数来解决,比如说上例中用到的字段筛选可以使用 selectarrow-up-right 函数来替代,即改成 Article.findOne({name:nameRand}).select('name -_id').exec(function(err,item) {}); 当中可以添加无数个链式函数来控制查询行为,比如说 limitarrow-up-right skiparrow-up-right leanarrow-up-right 等等,最后以 execarrow-up-right 函数结尾添加回调函数。mongoose 查询默认返回的是 MongooseDocumentsarrow-up-right 类型对象,使用lean 函数后可以将其转成普通 javascript 对象:

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

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

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

代码 5.2.1.14 mongoose 联合查询功能

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

5.3 代码

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

最后更新于