mongodb总结
基础概念
mongodb 的特点:
nosql数据库- 无
sql语句 - 使用极其简单,学习成本非常低
- 由于没有集合之间的关联,难以表达复杂的数据关系
- 存取速度极快
- 无
- 文档型数据库
- 数据内容非常丰富和灵活
- 对数据结构难以进行有效的限制
基础概念:
db:和mysql的概念一致collection:集合,类似于mysql中的表document:每个集合中的文档,类似于mysql中的记录Primary Key:和mysql中的主键含义一致,每个document都有一个主键field:文档中的字段
常用命令
-
查看所有数据库:
1
show dbs;
-
显示当前使用的数据库:
1
db;
-
查看当前数据库状态:
1
db.stats()
-
查看数据库中所有的集合:
1
show collections;
-
切换数据库:
1
use db;
-
备份:
1
mongodump -d <dbname> -o <backupDir>
-
恢复
1
mongorestore -d <dbname> <backupDir>
原生CRUD
Create
1 | // 新增单个数据,doc是一个文档对象 |
-
新的文档如果没有指定字段
_id,则会自动添加一个字段_id作为主键 -
自动的主键是一个
ObjectId对象,该对象是通过调用函数ObjectId()创建的 -
它的原理是根据时间戳+机器码+进程Id+自增量生成的一个十六进制的唯一字符串
-
使用
ObjectId函数还可以把某个字符串还原成一个ObjectId对象,例如ObjectId("xxxxx")
Read
1 | db.<collection>.find([filter, projection]); |
filter:条件参数projection:投影,表示哪些字段需要投影到查询结果中- 返回结果:一个游标对象(cursor),它类似于迭代器,可以在查询结果中进行迭代
cursor对象
next():游标向后移动,并返回下一个结果,如果没有结果则报错hasNext():判断游标是否还能向后移动skip(n):去前面的n条数据,返回cursorlimit(n):取当前结果的n条数据,返回cursorsort(sortObj):按照指定的条件排序,返回cursorcount():得到符合filter的结果数量size():得到最终结果的数量
由于某些函数会继续返回cursor,因此可以进行链式编程。
但是无论它们的调用顺序如何,始终按照这样的顺序执行:sort --> skip --> limit
count 始终返回的是 find 函数得到的数据数量,只有 size返回的是前一个函数返回的数量。
filter条件
filter的写法极其丰富,只总结常用的。
查询中的常用操作符:
$or:或者$and:并且$in:在…之中$nin:不在…之中$gt:大于$gte:大于等于$lt:小于$lte:小于等于$ne:不等于
常见场景:
-
查询所有
name为"曹敏" 并且age在 20~30 之间年龄的用户1
{ name: "曹敏", age: { $gt: 20, $lt: 30 } }
-
查询所有
age等于18 或 20 或 25 的用户1
{ age: { $in: [18, 20, 25] } }
-
查询所有
loginId以7结尾 或者name包含"敏"的用户1
{ $or: [ { loginId: /7$/ }, { name: /敏/ } ] }
-
查询
tags数组字段是否包含 “red”1
{ tags: "red" }
-
第四点具体来说,查询一个数组字段是否至少包含一个符合条件的元素,字段后面可以跟条件
dim_cm包含至少一个值大于25的元素1
{ dim_cm: { $gt: 25 } }
projection条件
类似于mysql中的select,表达了哪些字段需要投影到查询结果中,哪些不需要
-
查询结果中仅包含
name、age,以及会自动包含的_id1
{ name: 1, age: 1 }
-
查询结果不能包含
loginPwd、age,其他的都要包含1
{ loginPwd: 0, age: 0 }
Update
1 | db.<collection>.updateOne(filter, update, [options]); |
update操作
普通字段
-
将匹配文档的
name设置为"邓哥",address.city设置为"哈尔滨"1
{ $set: { name:"邓哥", "address.city": "哈尔滨" } }
-
将匹配文档的
age字段、address.province字段删除1
{ $unset: { age:"", "address.province":"" } }
-
将匹配文档的
age增加 2,number乘以21
{ $inc: { age: 2 }, $mul: { number: 2 } }
-
匹配文档的
name字段修改为fullname1
{ $rename: { name: "fullname" } }
数组字段
-
向
loves添加一项"秋葵",不存在则进行添加,若存在则不进行任何操作1
{ $addToSet: { loves: "秋葵" } }
-
向
loves添加一项"秋葵",无论数组中是否存在都添加1
{ $push: { loves: "秋葵" } }
-
向
loves添加多项:"秋葵、“香菜”1
{ $push: { loves: { $each: ["秋葵", "香菜"] } } }
-
删除
loves中满足条件的项: “秋葵” 或 “香菜”1
{ $pull: { loves: { $in: ["秋葵","香菜"] } } }
-
将所有
loves中的 "其他 "修改为 “other”1
2
3
4
5
6
7db.users.updateOne({
loves: "其他"
}, {
$set: {
"loves.$": "other"
}
})
option
upsert:默认false,若无法找到匹配项,则进行添加
Delete
1 | db.<collection>.deleteOne(filter) |
索引
在数据库中,索引类似于一个目录,用于快速定位到具体的内容
使用索引可以显著的提高查询效率,但会增加额外的存储空间
在mongodb中,索引的存储结构是B树。
创建索引
1 | db.<collection>.createIndex(keys, [options]); |
keys:指定索引中关联的字段,以及字段的排序方式,1为升序,-1为降序options:索引的配置background:默认false,建索引过程会阻塞其它数据库操作,是否以后台的形式创建索引unique:默认false,是否是唯一索引name:索引名称
其他索引操作
1 | // 查看所有索引 |
最佳实践
- 针对数据量大的集合使用索引
- 针对常用的查询或排序字段使用索引
- 尽量避免在程序运行过程中频繁创建和删除索引
Mongoose
CRUD操作
Create
方式1:创建模型对象,然后保存
1 | const obj = new <Model>(doc); |
方式2:直接使用函数创建对象
1 | // 创建一个或多个文档 |
创建操作的细节:
mongoose会为每个对象(包括子对象)添加字段_id,特别是在对象数组中,可以有效的维护数据的唯一标识- 如果希望禁用这种做法,只需要在相应的
Schema中配置_id: false
- 如果希望禁用这种做法,只需要在相应的
mongoose在创建文档时,会自动生成一个字段__v,该字段用于方式并发冲突- 如果希望禁用这种做法,只需要在
Schema的第二个参数中配置versionKey: false
- 如果希望禁用这种做法,只需要在
mongoose总是会在保存文档时触发验证,如果希望禁用这种行为,可以有两种做法:- 在
Schema的第二个参数中配置validateBeforeSave:false,该Schema的Model在保存时均不会触发验证 - 在调用
save方法或create方法时,传入配置validateBeforeSave:false,仅针对这一次调用不进行验证
- 在
mongoose支持<Model>.validate(doc, [context])直接对文档进行验证,该验证是异步的。<Model>.create(doc, option)等效于new <Model>(doc).save(option)- 如果给
create传入的是多个文档,则其在内部会创建多个模型,然后循环调用它们的save方法
- 如果给
- 两种方式都会得到模型实例,该实例会被
mongoose持续跟踪,只要对模型实例的修改都会被记录,一旦重新调用模型实例的save方法,就会把之前对模型的所有更改持久化到数据库。 - 新增对象时,如果遇到
Schema中没有定义的字段,则会被忽略
Read
1 | <Model>.findById(id); // 按照id查询单条数据 |
细节:
findOne 和 find如果没有给予回调或等待,则不会真正的进行查询,而是返回一个DocumentQuery对象,可以通过DocumentQuery对象进行链式调用进一步获取结果,直到传入了回调、等待、调用exec时,才会真正执行查询。
链式调用中包括:
count-->countDocumentslimitskipsort
和原生的区别:
count得到的是当前结果的数量- 查询
id时,使用字符串即可 projection支持字符串写法sort支持字符串写法populate支持关联查询
Update
方式1:在模型实例中进行更新,然后保存
1 | const u = await User.findById("5ed093872e3da2b654983476"); |
方式2:直接使用函数进行更新
1 | <Model>.updateOne(filter, update, [options]); |
细节:
_id可以直接使用字符串进行匹配updatec中可以省略$set,直接更改即可- 默认情况下,不会触发验证,需要在
options设置runValidators: true开启验证
Delete
1 | <Model>.deleteOne(filter); |
联表查询
- 定义
schema的时候,在需要联表查询的字段加入ref,值为模型名称 - 查询时,使用链式调用
populate(filed/options)
例如联表查询 user:
第一步,定义 schema:
1 | const categorySchema = new Schema<IMiniCategory>({ |
第二步,查询使用 populate:
1 | const result = await Article.find()populate("tags").populate({ |
并发管理
在并发请求中会有多个异步函数同时操作数据库,就可能发生数据模型和数据库中的数据不统一的情况,面对这种情况,mongoose作出以下假设:
- 当修改一个文档时,如果某些字段已经不再和数据库对应,说明这个字段的数据是脏数据(dirty data),对于脏数据,不应该对数据库产生影响
- 当修改一个文档时,如果字段和数据库是对应的,则是正常数据,正常数据可以正常的更改数据库
然而,mongoose无法准确的判定数组是否是脏数据,因此,如果遇到数组的修改,mongoose会做出如下处理:
- 当新增文档时,会自动添加字段
__v,用于记录更新版本号,一开始为0 - 通过模型实例对数组进行修改后,保存时会在内部调用实例的
increment函数,将版本号+1 - 当其他模型实例也更改了数组,保存时会对比版本号,如果不一致,则会引发
VersionError
出现错误是好事,可以提醒开发者:这一次保存的某些数据是脏数据,应该引起重视。开发者可以灵活的根据具体情况作出处理,比如提示用户保存失败,或者重新获取数据然后保存。
版本控制插件:
mongoose仅针对数组进行版本控制,如果要针对所有字段都进行版本控制,需要使用mongoose的插件:mongoose-update-if-current。
使用插件后,所有的字段都将受到版本控制,一旦版本不一致,将引发VersionError。




