MongoDB

一、介绍

1 基本介绍

MongoDB是一个基于分布式文件存储的数据库,由C++编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。它是一个面向文档(document-oriented)的数据库,而不是关系型数据库。它支持的数据结构非常松散,类似json格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,几乎可以实现类似关系数据库单表查询的绝大部分功能。

MongoDB作为一款通用型数据库,除了能够创建、读取、更新和删除数据之外,还提供了一系列不断扩展的独特功能:

  • 索引

    支持通用二级索引,允许多种快速查询,且提供唯一索引、复合索引、地理空间索引、全文索引

  • 聚合

    支持聚合管道,用户能通过简单的片段创建复杂的集合,并通过数据库自动优化

  • 特殊的集合类型

    支持存在时间有限的集合,适用于那些将在某个时刻过期的数据,如会话session。类似地,MongoDB也支持固定大小的集合,用于保存近期数据,如日志

  • 文件存储

    支持一种非常易用的协议,用于存储大文件和文件元数据。MongoDB并不具备一些在关系型数据库中很普遍的功能,如链接join和复杂的多行事务。省略这些的功能是处于架构上的考虑,或者说为了得到更好的扩展性,因为在分布式系统中这两个功能难以高效地实现

2 MongoDB和MySQL的对比

下面是MongoDB和关系型数据库MySQL对比

MySQL术语 MongoDB术语
database数据库 database数据库
table表 collection表(集合)
row行 document行(文档)
column字段 field字段
index索引 index索引
pk主键 pk主键(MongoDB将_id字段设置为主键)

在数据存储上,文档是MongoDB的核心概念,即采用类似json格式的键值对进行数据存储;

在配置文件上,MongoDB采用yaml格式文件配置。

二、安装和卸载

官网:https://www.mongodb.com/

下载地址:https://www.mongodb.com/try/download/community

MongoDB有两个版本,社区版和企业版,作为学习下载和安装社区版即可。

1 windows

打开下载地址之后,选择社区版,windows平台,如图:

image-20220120101620774

打开安装包,安装方式可以选择全部安装,如图:

image-20220120102003859

安装时可以启动一个服务,以及日志和数据的存放路径,可以手动修改,也可以保持默认:

image-20220120102057514

下面是MongoDB的可视化工具,这里不安装,后续有需要可以再安装:

image-20220120102116837

点击next开始安装,等待安装完毕即可。

2 linux

推荐使用yum安装,首先配置yum源。创建/etc/yum.repos.d/mongodb-org-5.0.repo 文件,内容如下:

1
2
3
4
5
6
[mongodb-org-5.0]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/5.0/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-5.0.asc

安装MongoDB的最新稳定版本,使用如下命令

1
yum install -y mongodb-org

默认情况下,MongoDB默认使用mongod用户运行,默认目录如下:

  • /var/lib/mongo数据目录
  • /var/log/mongodb 日志目录

包管理器在安装过程中创建上述默认目录。所有者和组名为mongod

安装完成后,通过以下命令启动mongod进程,

1
2
3
systemctl start mongod
# 如果你收到错误`Failed to start mongod.service: Unit mongod.service not found.`请先重启守护进程:
# systemctl daemon-reload

你可以通过以下命令来验证mongod进程是否已经成功启动:

1
systemctl status mongod

其他命令:

1
2
3
systemctl enable mongod  # 设置开机自启动
systemctl stop mongod # 停止mongod
systemctl restart mongod # 重启mongod

3 卸载

windows直接在控制面板卸载即可。这里介绍linux的卸载:

首先停止mongod服务

1
service mongod stop

删除以前安装的任何MongoDB包

1
yum erase $(rpm -qa | grep mongodb-org)

删除数据文件和日志文件

1
2
sudo rm -r /var/log/mongodb
sudo rm -r /var/lib/mongo

三、基本使用

官方文档:https://docs.mongodb.com/

接下来的内容在CentOS7环境下完成。

1 数据类型

MongoDB的数据类型为BSON,是一种二进制序列化格式,可以认为类似于JSON。

举例:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
#1、null:用于表示空或不存在的字段
d={'x':null}
#2、布尔型:true和false
d={'x':true,'y':false}
#3、数值
d={'x':3,'y':3.1415926}
#4、字符串
d={'x':'abc'}
#5、日期
d={'x':new Date()}
d.x.getHours()
#6、正则表达式
d={'pattern':/^*.?abc$/i}

# 正则写在//内,后面的i代表:
# i 忽略大小写
# m 多行匹配模式
# x 忽略非转义的空白字符
# s 单行匹配模式

#7、数组
d={'x':[1,'a','v']}

#8、内嵌文档
user={'name':'abc','addr':{'country':'China','xxxx':'xx'}}
user.addr.country

#9、对象id:是一个12字节的ID,是文档的唯一标识,不可变
d={'x':ObjectId()}

#10、ObjectId
ObjectId是"_id"的默认类型。因为设计MongoDb的初衷就是用作分布式数据库,所以能够在分片环境中生成唯一的标识符非常重要,而常规的做法在多个服务器上同步自动增加主键既费时又费力,这就是MongoDB采用ObjectId的原因。
ObjectId采用12字节的存储空间,是一个由24个十六进制数字组成的字符串
0|1|2|3| 4|5|6| 7|8 9|10|11
时间戳 机器 PID 计数器
如果快速创建多个ObjectId,会发现每次只有最后几位有变化。另外,中间的几位数字也会变化(要是在创建过程中停顿几秒)。
这是ObjectId的创建方式导致的

时间戳单位为秒,与随后5个字节组合起来,提供了秒级的唯一性。这个4个字节隐藏了文档的创建时间,绝大多数驱动程序都会提供
一个方法,用于从ObjectId中获取这些信息。

因为使用的是当前时间,很多用户担心要对服务器进行时钟同步。其实没必要,因为时间戳的实际值并不重要,只要它总是不停增加就好。
接下来3个字节是所在主机的唯一标识符。通常是机器主机名的散列值。这样就可以保证不同主机生成不同的ObjectId,不产生冲突

接下来两个字节确保了在同一台机器上并发的多个进程产生的ObjectId是唯一的

前9个字节确保了同一秒钟不同机器不同进程产生的ObjectId是唯一的。最后3个字节是一个自动增加的计数器。确保相同进程的同一秒产生的
ObjectId也是不一样的。

#11、自动生成_id
如果插入文档时没有"_id"键,系统会自动创建。可以将其作为分布式ID。
但通常会在客户端由驱动程序完成。这一做法非常好地体现了MongoDb的哲学:能交给客户端驱动程序来做的事情就不要交给服务器来做。
这种理念背后的原因是:即便是像MongoDB这样扩展性非常好的数据库,扩展应用层也要比扩展数据库层容易的多。将工作交给客户端做就
减轻了数据库扩展的负担。

2 数据库操作

连接mongod服务:

1
2
3
mongo --host localhost --port 27017
# 默认端口为27017,或者使用mongosh(mongo的升级版,有更强的功能)
mongosh --host localhost --port 27017

连接后,可以查看和切换数据库:

1
2
3
4
5
6
7
8
9
> show databases  # 查看数据库命令 可以简写为 show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
# 默认存在三个系统库
> use local # 切换到local库
switched to db local
> show tables # 查看表
startup_log

发现和mysql的命令差不多。

使用use命令时,如果数据库不存在,MongoDB会首次存储该数据库的数据时创建该数据库。因此,可以切换到不存在的数据库并执行插入操作,并不会报错,比如:

1
2
3
4
5
6
7
8
9
> use test
switched to db test

# db代表当前数据库,当前在哪个库下,db就代指这个库
> db.table1.insert({'aaa':123}) # 数据库test会在插入数据的时候创建,集合table1尚不存在,也会自动创建
WriteResult({ "nInserted" : 1 })

> db.dropDatabase() #删除当前库
{ "ok" : 1 }

3 集合操作

集合(表)操作

1
2
3
4
5
6
7
8
9
10
11
#1、添加表
db.createCollection("c1")

#2、查看表
> show collections
# 或者
> show tables

#3、删除表c1
> db.c1.drop()
true

4 文档操作

切换到test库:

1
use test

4.1 插入文档

插入单个文档

1
2
3
4
# 插入数据,下面的示例将一个新文档插入到inventory集合中。
db.inventory.insertOne(
{ item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } }
)

插入多个文档

1
2
3
4
5
6
# 下面的示例将3个新文档插入到inventory集合中。
db.inventory.insertMany([
{ item: "journal", qty: 25, tags: ["blank", "red"], size: { h: 14, w: 21, uom: "cm" } },
{ item: "mat", qty: 85, tags: ["gray"], size: { h: 27.9, w: 35.5, uom: "cm" } },
{ item: "mousepad", qty: 25, tags: ["gel", "blue"], size: { h: 19, w: 22.85, uom: "cm" } }
])

MongoDB文档与Javascript的对象相近,因此可以这样:

1
2
3
4
5
6
7
8
9
for(let i=1;i<=5;i++){
print(i);
}

# 插入n条数据
let n = 20;
for(var i=1;i<=n;i++){
db.inventory.insertOne({number:i})
}

4.2 查询文档

查询一个表(集合)中的所有文档,可以将一个空文档作为查询过滤器参数传递给查询函数

1
2
3
db.inventory.find({})
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory

指定相等条件的查询

1
2
3
db.inventory.find({item: "mat"})
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory WHERE item = "mat"

除了上述的基本查询,还可以使用比较运算符,如下表所示

运算符 说明
$eq 匹配等于指定值的值。
$gt 匹配大于指定值的值。
$gte 匹配大于或等于指定值的值。
$in 匹配数组中指定的任何值。
$lt 匹配小于指定值的值。
$lte 匹配小于或等于指定值的值。
$ne 匹配所有不等于指定值的值。

举例:

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
# 1 查询inventory集合中status字段为A或者D或者Z
db.inventory.find( { status: { $in: [ "A", "D", "Z"] } } )
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory WHERE status in ("A", "D", "Z")

# 2 查询inventory集合中status等于"A" 和qty小于30的所有文档:
db.inventory.find( { status: "A", qty: { $lt: 30 } } )
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory WHERE status = "A" AND qty < 30

# 3 查询inventory集合中status等于"A" 或qty小于30的所有文档:
db.inventory.find( { $or: [ { status: "A" }, { qty: { $lt: 30 } } ] } )
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory WHERE status = "A" OR qty < 30

# 4 复合查询,查询inventory集合中status等于"A" 并且qty小于30或者以字符p开头的所有文档
db.inventory.find( {
status: "A",
$or: [ { qty: { $lt: 30 } }, { item: /^p/ } ] # 使用`//`书写正则表达式进行正则匹配
} )
# 该操作对应如下 SQL 语句:
SELECT * FROM inventory WHERE status = "A" AND ( qty < 30 OR item LIKE "p%")

# 5 查看第4个爱好为tea的人,索引从0开始
db.user.find({"hobbies.3":'tea'})

查询时控制显示的列:

1
2
3
db.inventory.find({status:"A"},{'_id':0,'qty':1,'name':1})
# 0表示不显示,1表示显示
# 查询inventory下status为A的所有文档,只显示qty和name字段,不显示_id

除了比较运算符还有很多运算符,比如逻辑运算符:

运算符 说明
$and 返回与两个子句的条件匹配的所有文档。
$not 反转查询表达式的效果并返回与查询表达式不匹配的文档。
$nor 返回所有未能匹配两个子句的文档。
$or 返回与任一子句的条件匹配的所有文档。

更多运算符请参考官方文档

查询出的结果可以进行排序:

1
2
3
# 排序: 1代表升序,-1代表降序
db.user.find().sort({"name":1,})
db.user.find().sort({"age":-1,'_id':1})

查询结果进行分页:

1
2
3
# 分页: limit代表取多少个文档,skip代表跳过前多少个文档。 
# limit中表示一页显示的条数,skip(页码数*一页显示的条数)
db.user.find().sort({'age':1}).limit(1).skip(2)

查询结果计数:

1
2
3
4
# 使用count计数
db.user.count({'age':{"$gt":30}})
# 先查询再计数
db.user.find({'age':{"$gt":30}}).count()

4.3 更新文档

基本语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)

# query : 查询条件
# update : update的对象,配合一些更新运算符(如$,$inc...等)
# upsert : 可选,默认为false,代表如果不存在update的记录不更新也不插入,设置为true代表插入。
# multi : 可选,默认为false,代表只更新找到的第一条记录,设为true,代表更新找到的全部记录。
# writeConcern :可选,抛出异常的级别。

为了更新单个文档,MongoDB提供了更新操作符,例如$set,来修改字段值:

1
2
3
4
5
6
7
db.inventory.updateOne(
{ item: "paper" },
{
$set: { "size.uom": "cm", status: "P" },
$currentDate: { lastModified: true }
}
)

上述更新操作:

  • 使用$set运算符将字段size.uom的值更新为cm,将字段status的值更新为P
  • 使用$currentDate运算符将字段的值更新为lastModified当前日期。如果 lastModified字段不存在,将创建该字段。

相关的运算符如下:

运算符 说明
$currentDate 将字段的值设置为当前日期,可以是日期或时间戳。
$inc 将字段的值增加指定的数量。
$min 仅当指定值小于现有字段值时才更新字段。
$max 仅当指定值大于现有字段值时才更新字段。
$mul 将字段的值乘以指定的数量。
$rename 重命名字段。
$set 设置文档中字段的值。
$setOnInsert 如果更新操作导致插入文档,则设置字段的值。如果更新操作没有导致插入则什么都不做。
$unset 从文档中删除指定的字段。
$addToSet 仅当集合中尚不存在元素时,才将元素添加到数组中。
$pop 删除数组的第一项或最后一项。
$pull 删除与指定查询匹配的所有数组元素。
$push 将项目添加到数组。
$pullAll 从数组中删除所有匹配的值。

MongoDB提供以下方法来更新集合中的文档:

  • db.collection.updateOne():即使匹配多个文档,也最多更新单个文档
  • db.collection.updateMany():匹配多少文档就更新多少文档
  • db.collection.replaceOne():即使匹配多个文档,也只替换单个文档

使用举例:

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
# 1、$inc增加或者减少:将qty增加1
db.inventory.updateOne(
{item:'canvas'},
{
$inc:{qty:1}
}
)

# 2、所有匹配到的文档,qty减少5
db.inventory.updateMany(
{},
{
$inc:{qty:-5}
}
)

# 3、$push添加数组内元素,为匹配到的文档添加一个字段hobbies
db.inventory.updateOne(
{item:'canvas'},
{
"$push":{"hobbies":"game"}
}
)

# 4、$push和$pull按照条件删除元素(不删除字段),"$pull" 把符合条件的全部删除,$pop从两端删一个
db.inventory.updateMany(
{},
{
"$pull":{"hobbies":"game"}
}
)

# 5、$unset按照条件删除指定字段,空字符串""不会影响操作。如果该字段不存在,不执行任何操作
db.inventory.updateMany(
{},
{
"$unset":{"hobbies":""}
}
)

更多用例请参阅官方文档。

4.4 删除文档

提供两种删除方法:

  • db.collection.deleteMany():删除多个
  • db.collection.deleteOne():删除一个

要从集合中删除所有文档,请将一个空的过滤器{}传递给db.collection.deleteMany()方法

1
db.inventory.deleteMany({})

删除所有符合条件的文档:

1
2
db.inventory.deleteMany( {'qty': 100} )   # 删除所有qty为100的文档
db.inventory.deleteMany({ status : "A" }) # 删除所有status为A的文档

仅删除一个符合条件的文档:

1
db.inventory.deleteOne( {'qty': 100} )   # 即使匹配多个,也只删除一个qty为100的文档

5 聚合

聚合操作可以处理多个文档并返回计算结果。比如:

  • 将多个文档中的值组合在一起。
  • 对文档中的值进行运算操作并返回结果
  • 分析数据随时间的变化

要执行聚合操作,可以使用

聚合管道的每个阶段对输入文档执行一个操作。例如,一个阶段可以过滤文档、分组文档和算值。从一个阶段输出的文档将输入到下一个阶段。聚合管道可以返回文档组的结果。例如,返回总值、平均值、最大值和最小值。

创建用例:

1
2
3
4
5
6
7
8
db.orders.insertMany( [
{ _id: 0, productName: "Steel beam", status: "new", quantity: 10 },
{ _id: 1, productName: "Steel beam", status: "urgent", quantity: 20 },
{ _id: 2, productName: "Steel beam", status: "urgent", quantity: 30 },
{ _id: 3, productName: "Iron rod", status: "new", quantity: 15 },
{ _id: 4, productName: "Iron rod", status: "urgent", quantity: 50 },
{ _id: 5, productName: "Iron rod", status: "urgent", quantity: 10 }
] )

下面使用聚合管道,包含两个阶段并返回每个产品的紧急订单总数:

1
2
3
4
5
6
7
db.orders.aggregate( [
{ $match: { status: "urgent" } },
{ $group: { _id: "$productName", sumQuantity: { $sum: "$quantity" } } }
] )

# $match,筛选文档
# $group,分组

更多相关运算符可参阅官方文档

四、访问控制

MongoDB 使用基于角色的访问控制 (RBAC) 来管理对 MongoDB 系统的访问。用户被授予一个或多个角色,这些角色决定了用户对数据库资源和操作的访问权限。在角色分配之外,用户无权访问系统。

MongoDB默认不启用访问控制,可以使用--auth选项来开启。启用访问控制后,用户必须进行身份验证。

MongoDB的用户创建在库下,每个数据库都可以创建用户,一般来说,要管理哪个库,就在哪个库下创建账号。管理员账号建在amdin库下。

1 内置角色

介绍常用的内置角色。更多角色请参考这里

数据库用户角色:

  • read读权限,提供读取所有非系统集合上的数据的能力。

  • readWrite读写权限,包含read所有权限以及修改所有非系统集合上的数据的能力。

数据库管理角色:

  • dbAdmin提供执行管理任务的能力,例如与模式相关的任务、索引和收集统计信息。此角色不授予用户和角色管理权限。

  • userAdmin提供在当前数据库上创建和修改角色和用户的能力。

  • dbOwner数据库所有者可以对数据库执行任何管理操作。此角色结合了readWritedbAdminuserAdmin的权限。

集群管理角色:

  • hostManager提供监视和管理服务器的能力
  • clusterManager在集群上提供管理和监视操作。可以访问配置和本地数据库,这些数据库分别用于分片和复制
  • clusterMonitor提供对监控工具的只读访问
  • clusterAdmin提供最强大的集群管理访问(副本集、分片、主从等)。组合了clusterManagerclusterMonitorhostManager角色的能力,还提供了dropDatabase操作

备份恢复角色:

  • backup提供备份数据所需的能力
  • restore提供使用mongorestore恢复数据的能力

以下角色提供了为任何用户分配对任何数据库的任何特权的能力,这意味着具有这些角色之一的用户可以为自己分配对任何数据库的任何特权:

  • dbOwner角色作用于admin数据库时
  • userAdmin角色作用于admin数据库时
  • userAdminAnyDatabase角色

以下角色提供对所有资源的完全权限:

  • root

2 创建管理员用户

管理员用户在admin库下创建

1
2
3
4
5
6
7
8
> use admin
> db.createUser(
{
user: "root",
pwd: "123",
roles: [ { role: "root", db: "admin" } ]
}
)

其他普通用户的创建也是一样的,提供一个通用模版:

1
2
3
4
5
6
7
8
> db.createUser({
user:"xxx",
pwd:"xxxx",
customDate:"xxx",
roles:[{ #指定角色名称以及认证库
role:"xxx", db:"xxxx"
}]
})

3 开启访问控制

创建完成用户后,可以通过修改配置文件来启用RBAC,或者在命令行启动MongoDB时加上 -auth参数启动。这里选择前者:

1
2
3
4
vim /etc/mongod.conf  # mongdb的配置文件,是yaml格式
# 修改security.authorization
security:
authorization: enabled

补充:更多关于配置文件的内容,参阅官方文档:https://docs.mongodb.com/manual/reference/configuration-options/

重启mongod服务:

1
systemctl restart mongod

如果出现重启失败的情况,查看mongod状态:systemctl status mongod

如果有类似的错误Process: ***ExecStart=/usr/bin/mongod $OPTIONS (code=exited, status=14),通常是权限问题,mongod没有对必需文件的写权限,导致数据库服务不能启动。解决方案如下:

1
2
3
chown -R mongod:mongod /var/lib/mongo
chown -R mongod:mongod /var/log/mongodb
chown mongod:mongod /tmp/*.sock

重启后,尝试连接服务:

1
mongosh --host localhost --port 27017

发现不使用用户名和密码依然可以连接到数据库。但是没有权限查看数据库。

在登录时认证:

1
2
3
4
mongosh --host localhost --port 27017 -u root -p 123 --authenticationDatabase admin
# -u 指定用户名
# -p 指定密码
# --authenticationDatabase 指定认证到哪个库(因为mongodb基于库进行RBAC控制)

也可以先不认证,连接之后再认证:

1
2
3
mongosh --host localhost --port 27017 # 先不认证
> use admin # 切换到要认证的库
> db.auth("root","123") # 在哪个库创建的用户就需要使用哪个库进行认证

4 删除用户

使用如下命令:

1
> db.dropUser("root")

5 修改密码

使用如下命令:

1
db.changeUserPassword("user","new_passwd")

五、API

MongoDB官方提供了众多API方便不同语言调用,包括C,C++,GO,Java,Python,PHP,Node.js,C#等等。

你可以点击这里查看所支持的所有库。

1 Go

https://pkg.go.dev/go.mongodb.org/mongo-driver@v1.8.0#section-readme

2 Python

https://pymongo.readthedocs.io/en/stable/tutorial.html

3 Java

https://docs.mongodb.com/drivers/java-drivers/