一、基本常用命令
1.1 案例需求
存放文章评论的数据存放到 MongoDB 中,数据结构参考如下,其中数据库为 articledb,
专栏文章评论 | comment | ||
字段名称 | 字段含义 | 字段类型 | 备注 |
_id | ID | ObjectId或String | Mongo的主键的字段 |
articleid | 文章ID | String | |
content | 评论内容 | String | |
userid | 评论人ID | String | |
nickname | 评论人昵称 | String | |
createdatetime | 评论的日期时间 | Date | |
likenum | 点赞数 | Int32 | |
replynum | 回复数 | Int32 | |
state | 状态 | String | 0:不可见;1:可见; |
parentid | 上级ID | String | 如果为0表示文章的顶级评论 |
1.2 数据库操作
1.2.1 选择和创建数据库
在 MongoDB 中,集合只有在内容插入后才会创建。 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。
# 选择和创建数据库的语法格式
# 数据库名可以是满足以下条件的任意UTF-8字符串。
# 不能是空字符串("")
# 不得含有' '(空格)、.、$、/、\和\0 (空字符)
# 应全部小写
# 最多64字节
use 数据库名称# 如果数据库不存在则自动创建,例如,以下语句创建 spitdb 数据库
use articledb# 查看有权限查看的所有的数据库命令
show dbs 或 show databases# 查看正在使用的数据库命令
# MongoDB 中默认的数据库为 test,如果你没有选择数据库,集合将存放在 test 数据库中。
db
有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库,如下
admin: 从权限的角度来看,这是 "root" 数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
config: 当 Mongo 用于分片设置时,config 数据库在内部使用,用于保存分片的相关信息。
1.2.2 数据库的删除
# 会删除当前正在使用的库,主要用来删除已经持久化的数据库
db.dropDatabase()
1.3 集合操作
集合,类似关系型数据库中的表。可以显示的创建,也可以隐式的创建。
1.3.1 集合的显示创建
# 集合的命名规范
# 集合名不能是空字符串""。
# 集合名不能含有\0字符(空字符),这个字符表示集合名的结尾。
# 集合名不能以"system."开头,这是为系统集合保留的前缀。
# 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现$。# 基本语法格式如下
# name: 要创建的集合名称
db.createCollection(name)# 创建一个名为 mycollection 的普通集合
db.createCollection("mycollection")# 查看当前库中的表
show collections 或 show tables
1.3.2 集合的隐式创建
当向一个集合中插入一个文档的时候,如果集合不存在,则会自动创建集合。通常我们使用隐式创建文档即可。
1.3.3 集合的删除
# 语法格式如下
db.collection.drop() 或 db.集合.drop()# 要删除 mycollection 集合,
# 如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false
db.mycollection.drop()
1.4 文档操作
文档(document)的数据结构和 JSON 基本一样。所有存储在集合中的数据都是 BSON 格式。
1.4.1 文档的插入
1):单个文档的插入
使用 insert() 或 save() 方法向集合中插入文档,语法如下:
db.collection.insert(<document or array of documents>,{writeConcern: <document>,ordered: <boolean>}
)
Parameter | Type | Description |
document | document or array | 要插入到集合中的文档或文档数组。((json格式) |
writeConcern | document | 插入时性能和可靠性的级别,了解即可。 |
ordered | boolean | 可选。如果为真,则按顺序插入数组中的文档,如果其中一个文档出现错误,MongoDB将返回而不处理数组中的其余文档。如果为假,则执行无序插入,如果其中一个文档出现错误,则继续处理数组中的主文档。在版本2.6+中默认为true |
接下来我们要向 comment 的集合(表)中插入一条测试数据,执行下面语句:
# 示例代码
db.comment.insert({"articleid":"100000","content":"今天天气真好,阳光明媚","userid":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}
)
提示:
1)comment 集合如果不存在,则会隐式创建。
2)mongo 中的数字,默认情况下是 double 类型,如果要存整型,必须使用函数NumberInt(整型数字),否则取出来就有问题了。
3)插入当前日期使用 new Date()
4)插入的数据没有指定 _id ,会自动生成主键值
5)如果某字段没值,可以赋值为 null,或不写该字段。
出现下面的内容就证明插入成功了
注意:
1. 文档中的键/值对是有序的。
2. 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
3. MongoDB 区分类型和大小写。
4. MongoDB 的文档不能有重复的键。
5. 文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。
文档键命名规范:
1、键不能含有\0 (空字符)。这个字符用来表示键的结尾。
2、.和$有特别的意义,只有在特定环境下才能使用。
3、以下划线 "_" 开头的键是保留的(不是严格要求的)。
2):多个文档的插入
使用 insertMany() 方法向集合中插入多个文档,语法如下:
db.collection.insertMany([ <document 1> , <document 2>, ... ],{writeConcern: <document>,ordered: <boolean>}
)
接下来我们要向 comment 的集合(表)中插入多条文章评论,执行下面语句:
db.comment.insertMany([{"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上,健康很重要,一杯温水幸福你我他。","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-08-05T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"},{"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水,冬天喝温开水","userid":"1005","nickname":"伊人憔悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"},{"_id":"3","articleid":"100001","content":"我一直喝凉开水,冬天夏天都喝。","userid":"1004","nickname":"杰克船长","createdatetime":new Date("2019-08-06T01:05:06.321Z"),"likenum":NumberInt(666),"state":"1"},{"_id":"4","articleid":"100001","content":"专家说不能空腹吃饭,影响健康。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T08:18:35.288Z"),"likenum":NumberInt(2000),"state":"1"},{"_id":"5","articleid":"100001","content":"研究表明,刚烧开的水千万不能喝,因为烫嘴。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T11:01:02.521Z"),"likenum":NumberInt(3000),"state":"1"}
]);
提示:
如果插入时指定了 _id ,则主键就是该值。如果某条数据插入失败,将会终止插入,但已经插入成功的数据不会回滚掉。
因为批量插入由于数据较多容易出现失败,因此,可以使用 try catch 进行异常捕捉处理,测试的时候可以不处理。如(了解):
try {db.comment.insertMany([{"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上,健康很重要,一杯温水幸福你我他。","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-08-05T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"},{"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水,冬天喝温开水","userid":"1005","nickname":"伊人憔悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"},{"_id":"3","articleid":"100001","content":"我一直喝凉开水,冬天夏天都喝。","userid":"1004","nickname":"杰克船长","createdatetime":new Date("2019-08-06T01:05:06.321Z"),"likenum":NumberInt(666),"state":"1"},{"_id":"4","articleid":"100001","content":"专家说不能空腹吃饭,影响健康。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T08:18:35.288Z"),"likenum":NumberInt(2000),"state":"1"},{"_id":"5","articleid":"100001","content":"研究表明,刚烧开的水千万不能喝,因为烫嘴。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-06T11:01:02.521Z"),"likenum":NumberInt(3000),"state":"1"}]);
} catch (e) {print (e);
}
1.4.2 文档的查询
# 查询数据的语法格式如下:
db.collection.find(<query>, [projection])
Parameter | Type | Description |
query | document | 可选。使用查询运算符指定选择筛选器。若要返回集合中的所有文档,请省略此参数或传递空文档( {} )。 |
projection | document | 可选。指定要在与查询筛选器匹配的文档中返回的字段(投影)。若要返回匹配文档中的所有字段,请省略此参数。 |
1)查询所有
db.comment.find()
或
db.comment.find({})
这里你会发现每条文档会有一个叫 _id 的字段,这个相当于我们原来关系数据库中表的主键,当你在插入文档记录时没有指定该字段,MongoDB 会自动创建,其类型是 ObjectID 类型。
如果我们在插入文档记录时指定该字段也可以,其类型可以是 ObjectID 类型,也可以是MongoDB 支持的任意类型。
如果我想按一定条件来查询,比如我想查询 userid 为 1003 的记录,怎么办?很简单!只要在 find() 中添加参数即可,参数也是 json 格式,如下:
# 查询指定条件的数据
db.comment.find({userid:'1003'})
如果你只需要返回符合条件的第一条数据,我们可以使用 findOne 命令来实现,语法和 find 一样。
# 查询用户编号是 1003 的记录,但只最多返回符合条件的第一条记录:
db.comment.findOne({userid:'1003'})
2)投影查询
如果要查询结果返回部分字段,则需要使用投影查询(不显示所有字段,只显示指定的字段),其中 _id 字段会默认显示。
# 查询结果只显示 _id、userid、nickname 字段
db.comment.find({userid:"1003"},{userid:1,nickname:1})
# 查询结果只显示 、userid、nickname ,不显示 _id
db.comment.find({userid:"1003"},{userid:1,nickname:1,_id:0})
# 查询所有数据,但只显示 _id、userid、nickname
db.comment.find({},{userid:1,nickname:1})
1.4.3 文档的更新
更新文档的语法如下:
db.collection.update(query, update, options)//或
db.collection.update(<query>,<update>,{upsert: <boolean>,multi: <boolean>,writeConcern: <document>,collation: <document>,arrayFilters: [ <filterdocument1>, ... ],hint: <document|string> // Available starting in MongoDB 4.2}
)
只需要关注前四个参数就可以了
Parameter | Type | Description |
query | document | 更新的选择条件。可以使用与 find() 方法中相同的查询选择器,类似 sql update 查询内 where 后面的。在 3.0 版中进行了更改:当使用 upsert:true 执行 update() 时,如果查询使用点表示法在 _id 字段上指定条件,则 MongoDB 将拒绝插入新文档。 |
update | document or pipeline | 要应用的修改。该值可以是:包含更新运算符表达式的文档,或仅包含:对的替换文档 |
upsert | boolean | 可选。如果设置为 true,则在没有与查询条件匹配的文档时创建新文档。默认值为 false,如果找不到匹配项,则不会插入新文档。 |
multi | boolean | 可选。如果设置为 true,则更新符合查询条件的多个文档。如果设置为 false,则更新一个文档。默认值为 false。 |
writeConcern | document | 可选。表示写问题的文档。抛出异常的级别。 |
collation | document | 可选。指定要用于操作的校对规则。校对规则允许用户为字符串比较指定特定于语言的规则,例如字母大小写和重音标记的规则。 |
arrayFilters | array | 可选。一个筛选文档数组,用于确定要为数组字段上的更新操作修改哪些数组元素。 |
hint | Document or string | 可选。指定用于支持查询谓词的索引的文档或字符串。该选项可以采用索引规范文档或索引名称字符串。如果指定的索引不存在,则说明操作错误。例如,请参阅版本4中的“为更新操作指定提示。 |
1):覆盖的修改
# 修改_id为1的记录,点赞量为1001
db.comment.update({_id:"1"},{likenum:NumberInt(1001)})
执行完上面的语句后,我们会发现,这条文档除了 likenum 字段其它字段都不见了
2):局部修改
为了解决这个问题,我们需要使用修改器 $set 来实现,命令如下
# 修改 _id 为 2 的记录,浏览量为 889
db.comment.update({_id:"2"},{$set:{likenum:NumberInt(889)}})
这样执行就可以更新指定的字段了,如下所示
3):批量的修改
# 默认只修改第一条数据
db.comment.update({userid:"1003"},{$set:{nickname:"凯撒2"}})# 修改所有符合条件的数据
db.comment.update({userid:"1003"},{$set:{nickname:"凯撒大帝"}},{multi:true})
4):列值增长的修改
如果我们想实现对某列值在原有值的基础上进行增加或减少,可以使用 $inc 运算符来实现。
# 对3号数据的点赞数,每次递增1
db.comment.update({_id:"3"},{$inc:{likenum:NumberInt(1)}})
1.4.4 文档的删除
# 删除文档的语法结构
db.集合名称.remove(条件)# 将数据全部删除,请慎用
db.comment.remove({})# 删除_id=1的记录,输入以下语句
db.comment.remove({_id:"1"})
1.5 文档的分页查询
1.5.1 统计查询
统计查询使用 count() 方法,语法如下:
# 可选项 options 暂时不使用
# 默认情况下 count() 方法返回符合条件的全部记录条数
db.collection.count(query, options)
Parameter | Type | Description |
query | document | 查询选择条件。 |
options | document | 可选。用于修改计数的额外选项。 |
1):统计所有记录数
# 统计 comment 集合的所有的记录数
db.comment.count()
2):按条件统计记录数
# 统计 userid 为 1003 的记录条数
db.comment.count({userid:"1003"})
1.5.2 分页列表查询
可以使用 limit() 方法来读取指定数量的数据,使用 skip() 方法来跳过指定数量的数据。基本语法如下所示:
db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
如果你想返回指定条数的记录,可以在 find() 方法后调用 limit 来返回结果( TopN),默认值是 20,例如:
# 返回 3 条记录
db.comment.find().limit(3)
skip() 方法同样接受一个数字参数作为跳过的记录条数。(前 N 个不要),默认值是 0
# 跳过前 3 条记录
db.comment.find().skip(3)
分页查询,每页显示 2 条数据,第二页显示第 3 和第 4 条数据。
# 第一页
db.comment.find().skip(0).limit(2)
# 第二页
db.comment.find().skip(2).limit(2)
# 第三页
db.comment.find().skip(4).limit(2)
1.5.3 排序查询
sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。语法如下所示:
db.COLLECTION_NAME.find().sort({KEY:1})
或
db.集合名称.find().sort(排序方式)
# 对 userid 降序排列,并对访问量进行升序排列
db.comment.find().sort({userid:-1,likenum:1})
提示:
skip(),limilt(), sort() 三个方法放在一起执行的时候,执行的顺序是先 sort(),然后是 skip(),最后是显示的 limit(),和命令编写顺序无关。
1.6 文档的更多查询
1.6.1 复杂条件查询
MongoDB 的模糊查询是通过正则表达式的方式实现的。格式为:
# 正则表达式是 js 的语法,直接量的写法
db.collection.find({field:/正则表达式/})
或
db.集合.find({字段:/正则表达式/})
# 查询评论内容包含 “开水” 的所有文档
db.comment.find({content:/开水/})# 要查询评论的内容中以 “专家” 开头的
db.comment.find({content:/^专家/})
1.6.2 比较查询
# 大于: field > value
db.集合名称.find({ "field" : { $gt: value }}) # 小于:field < value
db.集合名称.find({ "field" : { $lt: value }}) # 大于等于: field >= value
db.集合名称.find({ "field" : { $gte: value }}) # 小于等于: field <= value
db.集合名称.find({ "field" : { $lte: value }}) # 不等于: field != value
db.集合名称.find({ "field" : { $ne: value }})
# :查询评论点赞数量大于 700 的记录
db.comment.find({likenum:{$gt:NumberInt(700)}})
1.6.3 包含查询
包含使用 $in 操作符,不包含使用 $nin 操作符
# 查询评论的集合中 userid 字段包含 1003 或 1004 的文档
db.comment.find({userid:{$in:["1003","1004"]}})# 查询评论集合中 userid 字段不包含 1003 和 1004 的文档
db.comment.find({userid:{$nin:["1003","1004"]}})
1.6.4 条件查询
我们如果需要查询同时满足两个以上条件,需要使用 $and 操作符将条件进行关联。(相当于 SQL 的 and)格式如下:
$and:[ { },{ },{ } ]
# 查询评论集合中likenum大于等于700 并且小于2000的文档
db.comment.find({$and:[{likenum:{$gte:NumberInt(700)}},{likenum:{$lt:NumberInt(2000)}}]})
如果两个以上条件之间是或者的关系,我们使用 $or 操作符进行关联,与前面 and 的使用方式相同格式为:
$or:[ { },{ },{ } ]
# 查询评论集合中 userid 为 1003,或者点赞数小于 1000 的文档记录
db.comment.find({$or:[ {userid:"1003"} ,{likenum:{$lt:1000} }]})
1.7 常用命令小节
# 选择切换数据库
use articledb# 插入数据
db.comment.insert({bson数据})# 查询所有数据
db.comment.find()# 条件查询数据
db.comment.find({条件})# 查询符合条件的第一条记录
db.comment.findOne({条件})# 查询符合条件的前几条记录
db.comment.find({条件}).limit(条数)# 查询符合条件的跳过的记录
db.comment.find({条件}).skip(条数)# 修改数据
db.comment.update({条件},{修改后的数据}) 或 db.comment.update({条件},{$set:{要修改部分的字段:数据})# 数据并自增某字段值
db.comment.update({条件},{$inc:{自增的字段:步进值}})# 删除数据
db.comment.remove({条件})# 统计查询
db.comment.count({条件})# 模糊查询
db.comment.find({字段名:/正则表达式/})# 条件比较运算
db.comment.find({字段名:{$gt:值}})# 包含查询
db.comment.find({字段名:{$in:[值1,值2]}}) 或 db.comment.find({字段名:{$nin:[值1,值2]}})# 条件连接查询
db.comment.find({$and:[{条件1},{条件2}]}) 或 db.comment.find({$or:[{条件1},{条件2}]})
二、索引
2.1 概述
在 MongoDB 中使用索引可以高效地执行查询。如果没有索引,MongoDB 必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
如果在查询时存在适当的索引,MongoDB 可以使用该索引限制必须检查的文档数。
索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB 还可以使用索引中的排序返回排序结果。
MongoDB 索引使用 B 树数据结构(确切的说是 B-Tree,MySQL 是 B+Tree)
2.2 索引的类型
2.2.1 单字段索引
MongoDB 支持在文档的单个字段上创建用户定义的升序/降序索引,称为单字段索引。
对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为 MongoDB 可以在任何方向上遍历索引。
2.2.2 符合索引
MongoDB 还支持多个字段的用户定义索引,即复合索引。
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由 { userid: 1, score: -1 } 组成,则索引首先按 userid 正序排序,然后在每个 userid 的值内,再在按 score 倒序排序。
2.2.3 其他索引
1):地理空间索引(Geospatial Index)
为了支持对地理空间坐标数据的有效查询,MongoDB 提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。
2:)文本索引(Text Indexes)
MongoDB 提供了一种文本索引类型,支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如 “the”、“a”、“or”),而将集合中的词作为词干,只存储根词。
3:)哈希索引(Hashed Indexes)
为了支持基于散列的分片,MongoDB 提供了散列索引类型,它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询。
2.3 索引的操作
2.3.1 查看索引
# 返回一个集合中的所有索引的数组
# MongoDB 3.0+ 版本以上支持
db.collection.getIndexes()
# 查看 comment 集合中所有的索引情况
> db.comment.getIndexes()
[{"v" : 2,"key" : {"_id" : 1},"name" : "_id_","ns" : "test.comment"}
]
结果中显示的是默认 _id 索引。
默认 _id 索引:MongoDB 在创建集合的过程中,在 _id 字段上创建一个唯一的索引,默认名字为 _id_ ,该索引可防止客户端插入两个具有相同值的文档,您不能在 _id 字段上删除此索引。
注意:该索引是唯一索引,因此值不能重复,即 _id 值不能重复的。在分片集群中,通常使用 _id 作为片键。
2.3.2 创建索引
# 在集合上创建索引
db.collection.createIndex(keys, options)
Parameter | Type | Description |
keys | document | 包含字段和值对的文档,其中字段是索引键,值描述该字段的索引类型。对于字段上的升序索引,请指定值1;对于降序索引,请指定值-1。比如: {字段:1或-1} ,其中1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。另外,MongoDB 支持几种不同的索引类型,包括文本、地理空间和哈希索引。 |
options | document | 可选。包含一组控制索引创建的选项的文档。有关详细信息,请参见选项详情列表。 |
options(更多选项)列表
Parameter | Type | Description |
background | Boolean | 建索引过程会阻塞其它数据库操作,background 可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为false。 |
unique | Boolean | 建立的索引是否唯一。指定为 true 创建唯一索引。默认值为false. |
name | string | 索引的名称。如果未指定,MongoDB 的通过连接索引的字段名和排序顺序生成一个索引名称。 |
dropDups | Boolean | 3.0+ 版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false |
sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为 true 的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false |
expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL 设定,设定集合的生存时间。 |
v | index version | 索引的版本号。默认的索引版本取决于 mongod 创建索引时运行的版本。 |
weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重 |
default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 |
language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language |
提示:
注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex() ,之后的版本使用了 db.collection.createIndex() 方法, ensureIndex() 还能用,但只是 createIndex() 的别名。
# 单字段索引示例,对 userid 字段按升序建立索引
> db.comment.createIndex({userid:1})
{"createdCollectionAutomatically" : false,"numIndexesBefore" : 1,"numIndexesAfter" : 2,"ok" : 1
}
# 查看刚才创建的索引,新创建的索引名称为 userid_1
> db.comment.getIndexes()
[{"v" : 2,"key" : {"_id" : 1},"name" : "_id_","ns" : "test.comment"},{"v" : 2,"key" : {"userid" : 1},"name" : "userid_1","ns" : "test.comment"}
]
# 复合索引示例,对 userid 和 nickname 同时按降序建立复合索引
> db.comment.createIndex({userid:1,nickname:-1})
{"createdCollectionAutomatically" : false,"numIndexesBefore" : 2,"numIndexesAfter" : 3,"ok" : 1
}
# 查看刚才创建的索引,新创建的索引名称为 userid_1_nickname_-1
> db.comment.getIndexes()
[{"v" : 2,"key" : {"_id" : 1},"name" : "_id_","ns" : "test.comment"},{"v" : 2,"key" : {"userid" : 1},"name" : "userid_1","ns" : "test.comment"},{"v" : 2,"key" : {"userid" : 1,"nickname" : -1},"name" : "userid_1_nickname_-1","ns" : "test.comment"}
]
2.3.3 删除索引
可以移除指定的索引,或移除所有索引
# 指定索引的移除
db.collection.dropIndex(index)# 所有索引的移除
db.collection.dropIndexes()
Parameter | Type | Description |
index | string or document | 指定要删除的索引。可以通过索引名称或索引规范文档指定索引。若要删除文本索引,请指定索引名称。 |
# 删除 comment 集合中 userid 字段上的升序索引
> db.comment.dropIndex({userid:1})
{ "nIndexesWas" : 3, "ok" : 1 }# 查看是否删除成功
> db.comment.getIndexes()
[{"v" : 2,"key" : {"_id" : 1},"name" : "_id_","ns" : "test.comment"},{"v" : 2,"key" : {"userid" : 1,"nickname" : -1},"name" : "userid_1_nickname_-1","ns" : "test.comment"}
]
# 删除 comment 集合中所有索引。
> db.comment.dropIndexes()
{"nIndexesWas" : 2,"msg" : "non-_id indexes dropped for collection","ok" : 1
}
# 查看还剩下哪些索引
# _id 的字段的索引是无法删除的,只能删除非 _id 字段的索引。
> db.comment.getIndexes()
[{"v" : 2,"key" : {"_id" : 1},"name" : "_id_","ns" : "test.comment"}
]
2.4 索引的使用
2.4.1 执行计划
分析查询性能(Analyze Query Performance)通常使用执行计划(解释计划、Explain Plan)来查看查询的情况,如查询耗费的时间、是否基于索引查询等。
那么,通常,我们想知道,建立的索引是否有效,效果如何,都需要通过执行计划查看。语法如下:
db.collection.find(query,options).explain(options)
# 查看根据 userid 查询数据的情况
# 其中:"stage" : "COLLSCAN", 表示全集合扫描
> db.comment.find({userid:"1003"}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.comment","indexFilterSet" : false,"parsedQuery" : {"userid" : {"$eq" : "1003"}},"winningPlan" : {"stage" : "COLLSCAN","filter" : {"userid" : {"$eq" : "1003"}},"direction" : "forward"},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "node1","port" : 27017,"version" : "4.0.28","gitVersion" : "af1a9dc12adcfa83cc19571cb3faba26eeddac92"},"ok" : 1
}
>
# 下面对userid建立索引
> db.comment.createIndex({userid:1})
{"createdCollectionAutomatically" : false,"numIndexesBefore" : 1,"numIndexesAfter" : 2,"ok" : 1
}
# 再次查看执行计划
# 其中:"stage" : "FETCH" 表示基于索引的扫描
> db.comment.find({userid:"1003"}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.comment","indexFilterSet" : false,"parsedQuery" : {"userid" : {"$eq" : "1003"}},"winningPlan" : {"stage" : "FETCH","inputStage" : {"stage" : "IXSCAN","keyPattern" : {"userid" : 1},"indexName" : "userid_1","isMultiKey" : false,"multiKeyPaths" : {"userid" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"userid" : ["[\"1003\", \"1003\"]"]}}},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "node1","port" : 27017,"version" : "4.0.28","gitVersion" : "af1a9dc12adcfa83cc19571cb3faba26eeddac92"},"ok" : 1
}
2.4.2 涵盖查询
当查询条件和查询的投影仅包含索引字段时,MongoDB 直接从索引返回结果,而不扫描任何文档或将文档带入内存。 这些覆盖的查询可以非常有效。
# 只查询 userid 字段
> db.comment.find({userid:"1003"},{userid:1,_id:0})
{ "userid" : "1003" }
{ "userid" : "1003" }
> db.comment.find({userid:"1003"},{userid:1,_id:0}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.comment","indexFilterSet" : false,"parsedQuery" : {"userid" : {"$eq" : "1003"}},"winningPlan" : {"stage" : "PROJECTION","transformBy" : {"userid" : 1,"_id" : 0},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"userid" : 1},"indexName" : "userid_1","isMultiKey" : false,"multiKeyPaths" : {"userid" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"userid" : ["[\"1003\", \"1003\"]"]}}},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "node1","port" : 27017,"version" : "4.0.28","gitVersion" : "af1a9dc12adcfa83cc19571cb3faba26eeddac92"},"ok" : 1
}
三、实战演练
3.1 需求分析
某头条的文章评论业务如下:
参考这篇文章实现以下功能
1):基本增删改查 API
2):根据文章 id 查询评论
3):评论点赞
3.2 表结构分析
数据库:articledb
专栏文章评论 | comment | ||
字段名称 | 字段含义 | 字段类型 | 备注 |
_id | ID | ObjectId 或 String | Mongo的主键的字段 |
articleid | 文章ID | String | |
content | 评论内容 | String | |
userid | 评论人ID | String | |
nickname | 评论人昵称 | String | |
createdatetime | 评论的日期时间 | Date | |
likenum | 点赞数 | Int32 | |
replynum | 回复数 | Int32 | |
state | 状态 | String | 0:不可见;1:可见; |
parentid | 上级ID | String | 如果为0表示文章的顶级评论 |
3.3 技术选型
3.3.1 mongodb-driver
mongodb-driver 是 mongo 官方推出的 java 连接 mongoDB 的驱动包,相当于 JDBC 驱动。
3.3.2 SpringDataMongoDB
SpringData 家族成员之一,用于操作 MongoDB 的持久层框架,封装了底层的 mongodb-driver。我们此次使用这个工具进行开发。
3.4 项目搭建
3.4.1 添加 maven 依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency></dependencies>
3.4.2 创建 application.yml
spring:# 数据源配置data:mongodb:# 主机地址host: 192.168.229.154# 数据库database: articledb# 默认端口是27017port: 27017# 也可以使用uri连接#uri: mongodb://192.168.40.134:27017/articledb
3.4.3 创建启动类
package cn.article;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class ArticleApplication {public static void main(String[] args) {SpringApplication.run(ArticleApplication.class, args);}
}
启动项目,看是否能正常启动,控制台没有错误。
3.4.4 创建评论实体类
在 cn.article.po 包下创建创建实体类 Comment ,如下:
package cn.article.po;import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;import java.time.LocalDateTime;
import java.util.Date;/*** @Document(collection="comment")* 把一个java类声明为mongodb的文档,可以通过collection参数指定这个类对应的文档。* @Document(collection="mongodb 对应 collection 名")* 若未加 @Document ,该 bean save 到 mongo 的 comment collection* 若添加 @Document ,则 save 到 comment collection* 可以省略,如果省略,则默认使用类名小写映射集合*/
@Document(collection="comment")
// 如果想要添加复合索引,则在类的上面使用 @CompoundIndex 标签,例如:@CompoundIndex( def = "{'userid': 1, 'nickname': -1}")
public class Comment {// @Id为主键标识,该属性的值会自动对应 mongodb 的主键字段 "_id",如果该属性名就叫 “id”,则该注解可以省略,否则必须写@Id// 主键private String id;// @Field 标签的意思是对应 mongodb 的字段的名字,如果一致,则无需该注解@Field("content")// 吐槽内容private String content;// 发布日期private Date publishtime;// 添加了一个单字段的索引@Indexed// 发布人IDprivate String userid;// 昵称private String nickname;// 评论的日期时间private LocalDateTime createdatetime;// 点赞数private Integer likenum;// 回复数private Integer replynum;// 状态private String state;// 上级IDprivate String parentid;// 标题IDprivate String articleid;// setter、getter、toString()
}
说明:
索引可以大大提升查询效率,一般在查询字段上添加索引,索引的添加可以通过 Mongo 的命令来添加,也可以在 Java 的实体类中通过注解添加。
1):单字段索引注解 @Indexed
声明该字段需要索引,建索引可以大大的提高查询效率。
# 使用 mongoDB 命令创建单字段索引
db.comment.createIndex({"userid":1})
2):复合索引注解 @CompoundIndex
复合索引的声明,建复合索引可以有效地提高多字段的查询效率。
# 使用 mongoDB 命令创建复合索引
db.comment.createIndex({"userid":1,"nickname":-1})
3.4.5 创建评论服务层
在 cn.article.dao 包下创建接口 CommentRepository,如下:
package cn.article.dao;import cn.article.po.Comment;
import org.springframework.data.mongodb.repository.MongoRepository;// 评论的持久层接口
public interface CommentRepository extends MongoRepository<Comment,String> {}
在 cn.article.service 包下创建类 CommentService,如下:
package cn.article.service;import cn.article.dao.CommentRepository;
import cn.article.po.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;// 评论的业务层
@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository;// 保存评论public void saveComment(Comment comment) {// 如果需要自定义主键,可以在这里指定主键;如果不指定主键,MongoDB 会自动生成主键commentRepository.save(comment);}// 更新评论public void updateComment(Comment comment) {commentRepository.save(comment);}// 根据id删除评论public void deleteCommentById(String id) {commentRepository.deleteById(id);}// 查询所有评论public List<Comment> findCommentList() {return commentRepository.findAll();}// 根据id查询评论public Comment findCommentById(String id) {return commentRepository.findById(id).get();}
}
新建 Junit 测试类 CommentServiceTest ,测试保存和查询所有,代码如下:
package cn.article.service;import cn.article.ArticleApplication;
import cn.article.po.Comment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;// 测试评论的业务层
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArticleApplication.class)
public class CommentServiceTest {@Autowiredprivate CommentService commentService;// 保存一条评论@Testpublic void testSaveComment(){Comment comment=new Comment();comment.setArticleid("100000");comment.setContent("测试添加的数据");comment.setCreatedatetime(LocalDateTime.now());comment.setUserid("1003");comment.setNickname("凯撒大帝");comment.setState("1");comment.setLikenum(0);comment.setReplynum(0);commentService.saveComment(comment);}// 查询所有数据@Testpublic void testFindAll(){List<Comment> list = commentService.findCommentList();System.out.println(list);}// 测试根据id查询@Testpublic void testFindCommentById(){Comment comment = commentService.findCommentById("1");System.out.println(comment);}
}
3.5 实现分页效果
接下来我们实现一个根据上级 ID 查询文章评论的分页列表的功能,首先在 CommentRepository 类中新增方法定义,如下:
// 根据父 id,查询子评论的分页列表
// 需要注意的是这个方法名是有规定的,必须是 findBy属性名,不是随便起的
Page<Comment> findByParentid (String parentid, Pageable pageable);
在 CommentService 类中新增方法,如下:
// 根据父id查询分页列表
public Page<Comment> findCommentListPageByParentid(String parentid, int page , int size){return commentRepository.findByParentid(parentid, PageRequest.of(page-1,size));
}
在 CommentServiceTest 测试类中添加测试代码,如下:
// 测试根据父id查询子评论的分页列表
@Test
public void testFindCommentListPageByParentid(){Page<Comment> pageResponse = commentService.findCommentListPageByParentid("3", 1, 2);System.out.println("----总记录数:"+pageResponse.getTotalElements());System.out.println("----当前页数据:"+pageResponse.getContent());
}
使用 compass 工具将 comment 里面的数据添加一个 parentid 属性,方便我们进行测试,添加结束后,执行执行方法,结果如下:
3.6 实现评论的点赞
我们先看一下以下点赞的临时示例代码: CommentService 新增 updateThumbup 方法,如下:
/*** 点赞-效率低* @param id*/
public void updateCommentThumbupToIncrementingOld(String id){Comment comment = CommentRepository.findById(id).get();comment.setLikenum(comment.getLikenum()+1);CommentRepository.save(comment);
}
以上方法虽然实现起来比较简单,但是执行效率并不高,因为我只需要将点赞数加 1 就可以了,没必要查询出所有字段修改后再更新所有字段。
我们可以使用 MongoTemplate 类来实现对某列的操作。修改 CommentService 类如下所示:
// 注入MongoTemplate
@Autowired
private MongoTemplate mongoTemplate;/*** 点赞数+1* @param id*/
public void updateCommentLikenum(String id){// 查询对象Query query=Query.query(Criteria.where("_id").is(id));// 更新对象Update update=new Update();// 局部更新,相当于$set// update.set(key,value)// 递增$inc// update.inc("likenum",1);update.inc("likenum");//参数1:查询对象;参数2:更新对象;参数3:集合的名字或实体类的类型Comment.classmongoTemplate.updateFirst(query,update,"comment");
}
在 CommentServiceTest 测试类中添加测试代码,如下:
/*** 点赞数+1*/
@Test
public void testUpdateCommentLikenum(){//对3号文档的点赞数+1commentService.updateCommentLikenum("4");
}
执行测试用例即可。