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
条数据,返回cursor
limit(n)
:取当前结果的n
条数据,返回cursor
sort(sortObj)
:按照指定的条件排序,返回cursor
count()
:得到符合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
,以及会自动包含的_id
1
{ 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
字段修改为fullname
1
{ $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
-->countDocuments
limit
skip
sort
和原生的区别:
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
。