MongoDB相关分享
以班车系统为例
文档型数据库MongoDB
先简单介绍下文档型数据库是什么。
面向文档的数据库(英语:Document-oriented database)或文档储存,是一种被设计用于储存、检索和管理文档导向信息(也称为“半结构化数据”)的计算机程序。文档导向的数据库是 NoSQL 数据库的一个主要类别,文档导向的数据库的普及程度已经随着 NoSQL 本身被不断使用而有所增长。 ——维基百科
文档型数据库与传统的关系型数据库存在着差异。
关系数据库通常将数据存储在相互独立的表格中,这些表格由程序开发者定义,单独一个的对象可以散布在若干表格中。 对于数据库中某单一实例中的一个给定对象,文档数据库存储其所有信息,并且每一个被存储的对象可与任一其它对象不同。这使得将对象映射入数据库简单化,并通常会消除任何类似于对象关系映射的事物。这也使得文档数据库对网络应用有较大价值,因为后者的数据处在不断变化中,而且对于后者来说,部署速度是一个重要问题。
关系型数据库存在ACID原则,即:
- 原子性(Atomic)
要么全做,要么全不做
- 一致性(Consistency)
数据在各个表中是一致的
- 隔离性(Identity)
两个对统一数据表操作时,通过锁机制防止重复写脏数据。
- 持久性(Consistency)
数据持久化,即数据最终是以文件的形式存在于文件系统中,不会由于系统崩溃或是突然断电等外部因素引发数据丢失。
文档型数据库主要有五个特点:
- 数据模型丰富
逻辑上是将对象按照文档模型来存储,同时支持了数组以及文档内嵌。文档的键不会事先定义,也不会固定不变,这就使得在需求经常变更情况下做修改更为便利,数据模型变更也更方便。
- 容易扩展
MongoDB支持主备切换,可以配置自动在多台服务器之间分割数据,以及进行主从复制,备份数据。它还可以平衡集群的数据和负载,自动重排文档。
- 功能丰富
支持索引、JavaScript函数、数据聚合功能等。文件存储是通过BSON格式,没有关系型数据库中常见联接(join)和事务。
- 不牺牲速度
MongoDB使用MongoDB传输协议作为与服务器交互的主要方式(与之对应的协议,像是HTTP/REST,需要更多的开销)。它对文档进行动态填充,预分配数据文件,用空间换取性能的文档。默认的存储引擎中使用了内存映射文件,将内存管理工作交给操作系统去处理。动态查询优化器会“记住”执行查询最高效的方式。总之,MongoDB在各方面都充分考虑了性能。
- 管理简便
MongoDB尽量让服务器自治来简化数据库的管理。除了启动数据库服务器之外,几乎没有什么必要的管理操作。如果主服务器挂掉了,MongoDB会自动切换到备份服务器上,并且将备份服务器提升为活跃服务器。在分布式环境下,集群只需要知道有新增加的节点,就会自动集成和配置新节点
与关系型数据库范式设计上的区别
一对一关系
与关系型数据库中的表示方式基本一致,在一个对象中新增对另一个对象的引用,通常是通过另一个对象的ID来表示。在这个关系上,两种数据库的设计模式一致。
一对多关系
与上述的关系基本一致,同样是和关系型数据库类似,在“多”对应的一方的文档上添加对“一”那一方的引用。
多对多关系
在关系型数据库的情况下,如果存在多对多关系,多对多关系本身会被单拿出来,由一个新的关系表来处理,每条记录代表一个一对一单向关系。由于MongoDB支持数组,因此不需要像关系型数据库那样新增一个关系表,来表示多对多关系。在对引用进行操作的情况(增加、修改)不多的情况下,可以使用双向嵌套,即是在两个文档上分别增加对对方的引用(通常是ID,也可以是内嵌)。
在需要对数据读取进行优化的场合(或者是对引用的操作不多的情况下),可以使用单向嵌套的方式,通常是属性相对较少的文档作为内嵌子文档。
举个栗子
项目涉及到的主要是用户、班车、站点、活动类型这4个实体,下面将逐个一一介绍:
用户类型
1 | const ActivityTypeSchema = new Schema({ |
此处描述的是仅有一个唯一键的字段name。由于MongoDB本身自带id属性,所以可以不考虑自己添加id字段。
站点
1 | const StationSchema = new Schema({ |
这里有四个属性,同时还有一个虚拟属性longlat,用于更方便的同时读取以及写入经纬度的两个属性。bus为一个数组,表示的是站点所属的班车线路,内部存放的是bus的id。
用户
1 | const UserSchema = new Schema({ |
residence其中内嵌了一个站点的子文档,familyMemberNums表示某位用户在某个线路上带家属的个数,bus也是一个数组,包括了上车的站点以及所在的班车路线。
班车
1 | const BusSchema = new Schema({ |
acitivityType为线路所属的活动类型id,体现的是活动类型对班车的一对多关系。而route和users两个数组则分别体现了与站点以及用户的多对多关系。enrollCount则是获取到报名人以及所带家属的人数。
在框架中的组织模式
Schema
这是一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力。
像是上面的这些都是Schema的主要内容,在模块中只要导出Schema对象以便在之后获取model对象并集成就好。
获取model
1 | const db = Object.create(null); |
mongoose.model获取的是由Schema发布生成的模型,具有抽象属性和行为的数据库操作。 可以用new 关键字model的方式创建实体(Entitiy)
建立数据库连接
1 | // mongoose support |
在app.js中连接数据库,并用ES6的Promise替换mongoose中原有的A+的Promise实现。最后,监听报错信息,若有报错则打印错误。
Mongoose的一些坑
虚拟属性
虚拟属性本身算是mongoose自带的特性,MongoDB中并没有对应的支持,对注册虚拟属性的文档本身的属性读取十分方便,但在子文档中就不是这样了。在github上的issue中似乎提到已经fix这个问题但在5.0.2rc下似乎仍存在这个问题。像是上面提及的id以及enrollCount这样的虚拟属性,在子文档中就无法获取到。
无事务支持
MongoDB这种NoSQL对数据一致性的支持不大好,主要表现在不支持关系型数据库中常见的事务功能。在使用mongoose时,很可能发生报错位置上的Promise中的then部分已经执行了,并且在报错后不会回滚。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42enroll(req, resp, next) {
const userName = req.session.user.userName;
const getBus = db.Bus.findById(req.body.busId);
getBus.then(bus => {
if(bus.activityType) {
return Promise.all([db.Bus.find({
activityType: bus.activityType,
}), bus]);
}else {
// 报名通勤班车的情况
}
}).then(([buses, targetBus]) => {
const getUser = db.User.findOne({ account: userName});
const getStation= db.Station.findById(req.body.stationId);
if(findIndex(buses, bus => findIndex(bus.users, ['account', userName]) >=0) >=0) {
resp.failed('您已在同活动类型下的其它班次报名过!请先取消报名');
}else if(req.body.stationId){
return Promise.all([getUser, getStation, targetBus]);
}else {
resp.failed('报名站点是必须的!');
}
}).then(([user, station, bus]) => {
if(bus && user && station && bus.enrollCount < bus.topLimit) {
// user.bus里没有当前bus,bus.users也没有当前用户
if(findIndex(user.bus, enrolledBus => enrolledBus.bus.equals(bus.id)) < 0 && findIndex(bus.users, ['account', userName] < 0)) {
user.bus.push({
station: station,
bus: bus.id,
});
user.familyMemberNums.push({ bus: bus.id, familyMemberNum: req.body.familyMemberNum });
bus.users.push(user);
return Promise.all([user.save(), bus.save()]);
}else {
resp.failed('报名失败,可能已经报名过');
}
}else {
resp.failed('当前报名人数已满');
}
}).then(() => {
resp.success(SUCCESS);
}).catch(next);
},
这里把之前Promise中获取到的targetBus对象,在最后一个Promise中和用户对象一起储存。而若是bus对象在之前的Promise中进行保存,那么假如user对象保存这一Promise失败的话,bus对象也已在之前的Promise中保存进数据库了,并且不能回退。把user和bus这样的有对数据库内容进行操作的步骤集中在一个Promise来完成,这样在某个Promise失败时也能确保另一个对数据库操作没有成功执行,在编码上实现了回退功能。
数据聚合
实际上是MongoDB自带的高级特性,mongoose中只是简单的封装了,在计算分页时比较方便,但是享受不到mongoose中虚拟属性的支持。
查询
- ID判等的时候使用equals方法。
enrolledBus.bus.equals(bus.id))
- 使用findById方法时必须传入id(id 不能为undefined),而在使用find方法通过id进行检索时,可以是undefined。
唯一键
设置属性unique:true
时,添加sparse:true
来防止空值重复。像是上面提到的班车线路的model中licenseNum车牌号属性,在设置了唯一键的情况、但不设置sparse:true
的情况,第一个没有licenseNum的班车线路可以正常保存,但在插入第二个没有车牌号的班车路线对象时则会提示,licenseNum为null重复。在加了sparse:true
后无此问题。
在公司网络环境下MongoDB的一些坑
版本
公司稳定版本3.4.2。奇数号版本是开发版本(比如3.5)。版本号不符合可能会收到来自安全产品部同事的请喝茶( ^‐^)_~~
绑定ip配置
默认bindIP设置为127.0.0.1,即仅允许来自本地的数据库连接,web应用与数据库不在同一主机上的话,可以修改bindIP为0.0.0.0,即允许来自任何ip的数据库连接请求。
用户认证配置
先将需要连接数据库用户在需要操作的数据库上创建,并赋予读写权限。1
2Authentication:
enable: true
然后在配置文件中添加以上认证配置。之后即可通过用户密码建立对数据库的连接。