MongoDB
一、基本使用
1.1业务应用场景
传统的关系型数据库(如Mysql),在数据库操作的“三高”需求以及对应web2.0的网站需求面前,显得力不从心
三高:
- High performance - 对数据库高并发读写的要求
- Huge Storage - 对海量数据的高效率存储和访问的需求
- High Scalability && High Avaliablity 对数据库的高可扩展性和高可用性的需求
具体的应用场景如:
- 社交场景,使用MongoDB存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
- 游戏场景,使用MongoDB存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
- 物流场景,使用MongoDB存储订单信息,订单状态在运送过程中会不断更新,以MongoDB内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
- 物联网场景,使用MongoDB存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
- 视频直播,使用MongoDB存储用户信息、点赞互动信息等。
这些应用场景中,数据操作方面的共同特点是:
- 数据量大
- 写入操作频繁(读写都很频繁)
- 价值较低的数据,对事务性要求不高
对于这样的数据,我们更适合使用MongoDB来实现数据的存储。什么时候选择MongoDB ?
在架构选型上,除了上述的三个特点外,如果你还犹豫是否要选择它?可以考虑以下的一些问题:应用不需要事务及复杂join支持
新应用,需求会变,数据模型无法确定,想快速迭代开发应用需要2000-3000以上的读写QPS(更高也可以)
应用需要TB甚至PB级别数据存储
那么我们什么时候选择 MongoDB 呢?
除了架构选型上, 除了上述三个特点之外, 还要考虑下面这些问题:
- 应用不需要事务及复杂 JOIN 支持
- 新应用, 需求会变, 数据模型无法确定, 想快速迭代开发
- 应用需要 2000 - 3000 以上的读写QPS(更高也可以)
- 应用需要 TB 甚至 PB 级别数据存储
- 应用发展迅速, 需要能快速水平扩展
- 应用要求存储的数据不丢失
- 应用需要
99.999%
高可用 - 应用需要大量的地理位置查询, 文本查询
如果上述有1个符合, 可以考虑 MongoDB, 2个及以上的符合, 选择 MongoDB 绝不会后悔.
如果用MySQL呢?
相对MySQL, 可以以更低的成本解决问题(包括学习, 开发, 运维等成本)
1.2 MongoDB 简介
MongoDB是一个开源, 高性能, 无模式的文档型数据库, 当初的设计就是用于简化开发和方便扩展, 是NoSQL数据库产品中的一种.是最 像关系型数据库(MySQL)的非关系型数据库. 它支持的数据结构非常松散, 是一种类似于 JSON 的 格式叫BSON, 所以它既可以存储比较复杂的数据类型, 又相当的灵活. MongoDB中的记录是一个文档, 它是一个由字段和值对(field:value)组成的数据结构.MongoDB文档类似于JSON对象, 即一个文档认 为就是一个对象.字段的数据类型是字符型, 它的值除了使用基本的一些类型外, 还可以包括其他文档, 普通数组和文档数组.
“最像关系型数据库的 NoSQL 数据库”. MongoDB 中的记录是一个文档, 是一个 key-value pair. 字段的数据类型是字符型, 值除了使用基本的一些类型以外, 还包括其它文档, 普通数组以及文档数组
MongoDB 数据模型是面向文档的, 所谓文档就是一种类似于 JSON 的结构, 简单理解 MongoDB 这个数据库中存在的是各种各样的 JSON(BSON)
- 数据库 (database)
- 数据库是一个仓库, 存储集合 (collection)
- 集合 (collection)
- 类似于数组, 在集合中存放文档
- 文档 (document)
- 文档型数据库的最小单位, 通常情况, 我们存储和操作的内容都是文档
在 MongoDB 中, 数据库和集合都不需要手动创建, 当我们创建文档时, 如果文档所在的集合或者数据库不存在, 则会自动创建数据库或者集合
1.3Windows下载
https://www.mongodb.com/try/download/community
1.4Linux下载
https://www.mongodb.com/try/download/community
1.5图形化界面
https://www.mongodb.com/try/download/compass
二、单机部署
三、基本使用
3.1案例需求
3.2数据库操作
- 设置MongoDB账号密码,防止被删库勒索
- 进入到mongodb服务器下
use admindb.createUser({user: '用户名', // 用户名(自定义)pwd: '密码', // 密码(自定义)roles:[{role: 'root', // 使用超级用户角色db: 'admin' // 指定数据库}]
})
- 找到配置的
mongodb.conf
文件,添加以下信息
security:authorization: enabled
- 重启mongodb服务
- 使用mongodb连接数据库,并使用超级管理员账号
在没有认证之前,也可以使用mongo链接数据库,但是不能执行其他命令
以下是两种使用超级管理员账号登录数据库的方式
// 方式一
mongo
use admin
db.auth('用户名', '密码')// 方式二
mongo admin -u 用户名 -p 密码
3.2.1权限说明(基于角色的权限控制)
3.2.1.1 内置角色
数据库用户角色
- read: 只读数据权限
- readWrite:写数据权限
数据库管理角色
- dbAdmin: 在当前db中执行管理操作的权限
- dbOwner: 在当前db中执行任意操作
- userADmin: 在当前db中管理user的权限
备份和还原角色
- backup
- restore
夸库角色
- readAnyDatabase: 在所有数据库上都有读取数据的权限
- readWriteAnyDatabase: 在所有数据库上都有读写数据的权限
- userAdminAnyDatabase: 在所有数据库上都有管理user的权限
- dbAdminAnyDatabase: 管理所有数据库的权限
集群管理
- clusterAdmin: 管理机器的最高权限
- clusterManager: 管理和监控集群的权限
- clusterMonitor: 监控集群的权限
- hostManager: 管理Server
超级权限
-
root: 超级用户
-
显示数据库
show dbs
3.2.1选择和创建数据库
选择和创建数据库
use 数据库名称
如果数据库不存在将会自动创建,比如将创建spitdb数据库
use spitdb
查看当前数据库
db
默认的三个数据库
- admin:从权限的角度看,这是root数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库权限。一些特定的服务器端命令也只能通过这个数据库运行,比如列出所有的数据库或者关闭服务器
- local:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
- config:当Mongo分片设置时,config数据库在内部使用,用于保存分片的相关信息
3.2.2数据库的删除
db.dropDatabase()
3.3集合操作
3.2.1集合的显示创建
db.createCollection(name) #name必须要由 "" 包围
查看当前库中的表
show tables
3.2.2集合的隐式创建
当向有一个集合中插入一个文档的时候,如果集合不存在,将会自动创建集合
3.2.3集合的删除
db.collection.drop()
或
db.集合name.drop()
返回值
如果成功删除选定的集合,则drop()方法返回true,否则返回false
例如:要删除mycollection集合
db.mycollection.drop()
3.4文档基本CRUD
文档(document)的数据结构和JSON基本一样
所有存储在集合中的数据都是JSON格式
3.4.1文档的插入
- 单个文档插入
使用insert() 或 save()方法向集合中插入文档,语法:
db.collection.insert(<document or array of documents>,{writeConcern: <document>,ordered: <boolean>}
)
参数
Paramter | Type | 描述 |
---|---|---|
document | document or array | 要插入到集合内的文档或文档数组(JSON格式) |
writeConcern | document | |
orderd | boolean | 可选。true代表按顺序插入数组中的文档,如果其中一个文档出现错误,MongoDB将返回而不处理数组中的其他文档;如果为false,则执行顺序插入,如果其中一个文档出现错误,则继续处理数组中的主文档。在2.6+版本,默认为true |
示例
db.comment.insert({"articleid":"100000","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}
)
提示
- comment如果不存在,将会被隐式创建
- mongodb中的数字,默认情况下时
double
类型,如果要存入整形,必须使用函数Number(整型数字)
,否则取出来会有问题 - 插入当前日期使用
new Date()
- 插入的数据没有指定
_id
,会自动生成主键值 - 如果某字段没值,可以赋值为null,或不写
插入成功后的返回值
WriteResult({ "nInserted" : 1 })
- 多个文档插入
db.collection.insert([<document or array of documents>,{writeConcern: <document>,ordered: <boolean>},{writeConcern: <document>,ordered: <boolean>},{writeConcern: <document>,ordered: <boolean>},....])
db.comment.insert([{"articleid":"100001","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100002","content":"今天天气真好","userId":"1002","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100003","content":"今天天气真好","userId":"1003","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100004","content":"今天天气真好","userId":"1004","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}
]);
如果担心多个数据插入查询失败,可以加上try-catch
try{db.comment.insert([{"articleid":"100001","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100002","content":"今天天气真好","userId":"1002","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100003","content":"今天天气真好","userId":"1003","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100004","content":"今天天气真好","userId":"1004","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}]);
}catch(e){print(e);
}
3.4.2文档的查询
db.collection.find(<query>,[projection])
参数:
Parameter | Type | Description |
---|---|---|
query | document | 可选,使用查询运算符指定选择筛选器。若要返回集合中的所有文档,请省略此参数或传递空文档 |
projection | document | 可选。指定要在于查询结果筛选器匹配的文档中返回的字段(投影)。若要返回匹配文档中的所有字段,请省略此参数 |
实例
db.comment.find()
或
db.comment.find({})#查询所有
db.comment.find(){ "_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"), "articleid" : "100000", "content" : "今天天气真好", "userId" : "1001", "nickname" : "Rose", "createdatetime" : ISODate("2024-09-03T07:10:52.544Z"), "likenum" : 10, "state" : null }#查询指定内容
db.comment.find({userId:"1001"}){ "_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"), "articleid" : "100000", "content" : "今天天气真好", "userId" : "1001", "nickname" : "Rose", "createdatetime" : ISODate("2024-09-03T07:10:52.544Z"), "likenum" : 10, "state" : null }#格式
db.comment.findOne({userId:"1001"})
{"_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"),"articleid" : "100000","content" : "今天天气真好","userId" : "1001","nickname" : "Rose","createdatetime" : ISODate("2024-09-03T07:10:52.544Z"),"likenum" : 10,"state" : null
}#指定查询字段
db.comment.findOne({userId:"1001"},{articleid:1,content:1})
{"_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"),"articleid" : "100000","content" : "今天天气真好"
}#排除指定字段
db.comment.findOne({userId:"1001"},{articleid:1,content:1,_id:0})
{ "articleid" : "100000", "content" : "今天天气真好" }
3.4.3文档的更新
db.collection.update(query,update,options)
或
db.collection.uodate(<query>,<update>,{upsert: <boolean>,multi:<boolean>,writeConcern:<document>collation: [<filterdocument1>,...],hint:<document|string> }
)
示例
- 覆盖的修改
如果我们想修改id为1的记录,点赞量为1001,输入一下语句
db.comment.update({id:"1",{likenum:NumberInt(1001)})
执行后我们会发现,这条字段除了likenum,其他字段都不见了
- 局部修改
db.comment.update({id:"1",{{$set:likenum:NumberInt(1001)}})
- 批量修改
db.comment.update({id:"1",{{$set:likenum:NumberInt(1001)}},{multi:true})
如果不加参数,只修改第一条
- 列值增长的修改
如果我们想实现对某列值在原有值基础上进行增加或减少,可以使用$inc
运算符来实现
需求:对3号数据的点赞数,每次递增1
db.comment.update({id:"3",{{$inc:likenum:NumberInt(1)}})
3.4.4文档的删除
db.集合名称.remove(条件)
以下为将数据全部删除
db.comment.remove({})
如果删除_id=1的记录,输入以下:
db.comment.remove({_id:"1"})
3.5文档的分页查询
3.5.1统计查询
统计查询使用count()方法
db.collection.count(query,options)
示例:
db.comment.count(Userid:"1003")
3.5.2列表查询
使用limit()可以限制查询的条数,使用skip()方法来跳过指定数量的数据
db.collection.find(x)
skip()
db.comment.find().skip(x)
例子
- 查找前四条数据
db.comment.find().limit(4)
- 查找第2条到最后一条数据
db.comment.find.skip(2)
- 查找第2条到第4条数据
#跳过前两条记录,查询两条记录
db.comment.find().skip(2).limit(2)
3.5.5排序查询
sort()方法对数据进行排序,sort()方法可以通过参数指定排序的字段,并使用1和-1来指定排序的方式,其中1为升序排序,-1位降序排序
#升序
db.collection.find().sort({key:1})#降序
db.collection.find().sort({key:-1})
例如
对userId进行降序排序
db.comment.find({},{userid:1}),sort({key:-1})
多个条件时
db.comment.find({},{userId:1,likenum:1}).sort(userId:-1,likenum:1)
3.6文档的更多查询
3.6.1正则的复杂条件查询
MongoDB的模糊查询是通过正则表达式的方式实现的。格式为:
db.collection.find({field:/正则表达式/})
或
db.集合.find({字段:/正则表达式/})
提示:正则表达式是js的语法,直接量的写法
例如,我要查询评论内容包含“开水”的所有文档,代码如下
db.comment.find({content:/开水/})
如果要查询评论的内容中以“专家”开头的,代码如下
db.comment.find({content:/^专家/})
3.6.2比较查询
<,<=,>,>=这些操作符也是很常用的,格式如下
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}}) //不等于: field != value
例子:查询评论点赞数量大于700的记录
db.comment.find({likenum:{$gt:NumberInt(7000)}})
3.6.3包含查询
使用$in
**示例:**查询评论的集合中userId字段包含1003或1004的文档
db.comment.find({userId:{$in["1003","1004"]}})
不包含$nIn
示例:
db.comment.find({userId:{$nin:["1003","1004"]}})
3.6.5条件连接查询
我们如果需要查询同时满足两个以上条件,需要使用$and操作符
$and:[{},{}]
**示例:**查询评论集合中likenum大于等于700并且小于2000的文档:
db.comment.find({$and:[{likenum:{$gte:NumberInt(700)},{likenum:{$it:NumberInt(2000)}}}]})
如果两个以上的条件时或者的关系,我们使用$or操作符
$or:[{},{}]
**示例:**查询评论集合中userId为1003,或者点赞数小于1000的文档
db.comment.find({$or:[{userId:"1003"},{likenum:{$lt:1000}}]})
四、索引
4.1概述
索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
如果查询存在适当的索引,MongoDB可以使用该索引限制必须检查的文档数。
索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB丕可以使用索引中的排序返回排序结果。
官网文档: https://docs.mongodb.com/manual/indexes/了解:
MogoDB索引使用B树数据结构(确切的说是B-Tree,MySQL是B+Tree)
4.2索引的类型
4.2.1单字段索引
MongoDB支持在文档的单个字段上创建用户定义的升序/降序索引,称为单字段索引(Single Field lndex)。对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为MongoDB可以在任何方向上遍历索引。
4.2.2复合索引
MongoDB还支持多个字段的用户定义索引,即复合索引(Compound Index)。
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由{ userid: 1, score: -1}组成,则索引首先按userid正序排序,然后在每个userid的值内,再在按score倒序排序。
4.2.3其他索引
地理空间索引(Geospatial lndex)、文本索引(Text Indexes)、哈希索引 (Hashed lndexes)。
- 地理空间索引 (Geospatial lndex):为了支持对地理空间坐标数据的有效查询,MongoDB提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。
- 文本索引(Text Indexes):MongoDB提供了一种文本索引类型,支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如如"the"、“a””、“or”),而将集合中的词作为词干,只存储根词。
- 哈希索引 (Hashed lndexes):为了支持基于散列的分片,MongoDB提供了散列索引类型,他对字段的散列进行索引,这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询
4.3索引的管理操作
4.3.1索引的查看
返回一个集合中的所有索引的数组
db.collection.getIndex()
仅MongoDB3.0+支持
**例子:**查看comment集合中所有的索引情况
db.comment.getIndexes()
结果中显示的是默认_id
索引
4.3.2索引的创建
- 单个索引
db.collection.createIndex(keys,options)
- 复合索引
# 根据userId升序,nickname降序创建
db.comment.createIndex({userId:1,nickname:-1})
4.3.3索引的移除
- 指定索引的移除
db.collection.dropIndex(index)
示例:
db.comment.dropIndex({userId:1})
- 移除所有索引
db.collection.dropIndexes()
_id
是不能被删除的,可以删除其他字段的索引
4.4索引的使用
4.4.1执行计划
类似mysql的执行计划,用来分析性能,通常使用执行计划来查看情况
db.collection.find(query,options).explain(options)
**示例:**根据userId查询数据的情况
db.comment.find({userId:"1003"}).explain()
> db.comment.find({userId:"1003"}).explain()
{"explainVersion" : "1","queryPlanner" : {"namespace" : "my.comment","indexFilterSet" : false,"parsedQuery" : {"userId" : {"$eq" : "1003"}},"queryHash" : "B1777DBA","planCacheKey" : "A6C2ADF9","maxIndexedOrSolutionsReached" : false,"maxIndexedAndSolutionsReached" : false,"maxScansToExplodeReached" : false,"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" : [ ]},"command" : {"find" : "comment","filter" : {"userId" : "1003"},"$db" : "my"},"serverInfo" : {"host" : "VM-24-17-centos","port" : 27017,"version" : "5.0.11","gitVersion" : "d08c3c41c105cde798ca934e3ac3426ac11b57c3"},"serverParameters" : {"internalQueryFacetBufferSizeBytes" : 104857600,"internalQueryFacetMaxOutputDocSizeBytes" : 104857600,"internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600,"internalDocumentSourceGroupMaxMemoryBytes" : 104857600,"internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600,"internalQueryProhibitBlockingMergeOnMongoS" : 0,"internalQueryMaxAddToSetBytes" : 104857600,"internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600},"ok" : 1
}
MongoDB Compass查看
4.4.2涵盖的查询
当查询掉件和查询的投影仅包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存,这些覆盖的查询可以非常有效
db.comment.find({userId:"1003"},{userId,_id:0})
五、案例:文章评论
5.1需求分析
5.2表结构分析
数据库:articledb
字段名称 | 作用 | 字段类型 | 备注 |
---|---|---|---|
_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表示文章的置顶评论 |
5.3技术选型
以下为SpringBoot项目搭建
5.3.1mongodb-driver
java链接的,类似于JDBC
5.3.2SpringDataMongoDB
SpringData家族成员之一,底部封装了mongodb-driver
5.4搭建Java环境
- maven
<!-- Mongodb--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!-- Mongodb-->
- yaml
spring:data:mongodb:host: 101.43.254.109database: articledbport: 27017#有账号密码就写,没有就注掉username: "用户名"password: "密码"#因为配置过对所有数据库通用的账号,所以这里加上authentication-database: admin#也可以使用url连接#uri:
5.5编写实体类
- pojo
package com.example.pojo;import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
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.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:27*/@Data
@ToString
//类名是大写,这里是为了和数据库对应
@Document(collection = "comment")
//新增复合索引,最好还是在命令行的方式建
//@CompoundIndex(def = "{'userId':1,'nickname':-1}")
public class Comment implements Serializable {//主键 该属性的值会自动对应mongodb的主键字段“_id",如果该属性名就交”id“,则该注解可以省略,否则必须写@Idprivate String id;//该属性对应mongodb的字段的名称,如果一致就不用写@Field("content")//内容private String content;//发布日期private Date publishTime;//发布人Id@Indexed //添加一个单字段的索引private String userId;//昵称private String nickname;//评论人日期private LocalDateTime createDatetime;//点赞数private Integer likeNum;//回复数private Integer replyNum;//状态private String state;//上级IDprivate String parentId;//文章IDprivate String articleId;}
5.6CRUD
- dao
package com.example.demo.dao;import com.example.pojo.Comment;
import org.springframework.data.mongodb.repository.MongoRepository;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:58*/
public interface CommentRepository extends MongoRepository<Comment,String> {
}
- service
package com.example.demo.service;import com.example.demo.dao.CommentRepository;
import com.example.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:59*/
@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository ;/*** 保存* @param comment c* @return comment*/public Comment saveComment(Comment comment){return commentRepository.save(comment);}/*** 更新* @param comment c* @return comment*/public Comment updateComment(Comment comment){return commentRepository.save(comment);}/*** 删除* @param id id*/public void deleteCommentById(String id){commentRepository.deleteById(id);}/*** 查询所有* @return list*/public List<Comment> findCommentList(){return commentRepository.findAll();}/*** 根据id查询频率* @param id id* @return comment*/public Comment findCommentByID(String id){return commentRepository.findById(id).get();}
}
- Test
package com.example.demo.service;import com.example.pojo.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;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 16:09*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class CommentServiceTest {@Autowiredprivate CommentService commentService;@Testpublic void findCommentList(){List<Comment> commentList = commentService.findCommentList();for (Comment comment : commentList) {System.out.println(comment);}}@Testpublic void save(){Comment comment = new Comment();comment.setArticleId("100002");comment.setContent("我不爱喝水");comment.setUserId("1005");comment.setNickname("海绵宝宝");comment.setCreateDatetime(LocalDateTime.now());comment.setLikeNum(6000);comment.setState("1");commentService.saveComment(comment);}@Testpublic void findById(){//Comment(id=6315bb3efabdb55ab0d8ab2f, content=我爱喝水, publishTime=null, userId=1004, nickname=杰克船长, createDatetime=2024-09-05T17:02:54.301, likeNum=2000, replyNum=null, state=1, parentId=null, articleId=100001)System.out.println(commentService.findCommentByID("6315bb3efabdb55ab0d8ab2f"));}
}
5.7根据上级ID查询文章评论的分页列表
- CommentRepository新增
Page<Comment> findByParentId(String parentId, Pageable pageable);
- CommentService新增
/*** 分页查询* @param parentId 父级id* @param page 页码* @param size 每页size* @return comment*/public Page<Comment> findCommentListByParentId(String parentId,int page,int size){return commentRepository.findByParentId(parentId, PageRequest.of(page-1, size));}
- Test
@Testpublic void findByParentId(){Page<Comment> page = commentService.findCommentListByParentId("10004", 1, 4);System.out.println("total:"+page.getTotalElements());List<Comment> content = page.getContent();System.out.println("all:"+page.getContent());for (Comment comment : content) {System.out.println(comment);}}
5.8MongoTemplate实现评论点赞
我们看以下节点的临时示例代码:CommentService新增updateThumbup方法
/*** 评论点赞* 效率低*/public void updateCommentThumbupToIncrementingOld(String id){Comment comment = commentRepository.findById(id).get();comment.setLikeNum(comment.getLikeNum()+1);commentRepository.save(comment);}
以上方法虽然实现起来比较简单,但是执行效率比较高,因为我们只需要将点赞数+1就可以了,没必要查询出所有字段修改后再更新所有字段
优化
我们可以使用MongoTemplate类来实现对某列的操作
- 修改CommentService
@Autowiredprivate MongoTemplate mongoTemplate;/*** 点赞优化* @param id id*/public void updateCommentLikeNum(String id){//查询条件 数据库里的id 等于传进来的id时Query query = Query.query(Criteria.where("_id").is(id));//更新条件Update update = new Update();//指定字段为likenum,步长为1update.inc("likeNum",1);//指定query、update、指定表<comment.class>mongoTemplate.updateFirst(query,update,Comment.class);}
- 测试
@Testpublic void updateCommentLikeNum(){commentService.updateCommentLikeNum("6315bfcb234e2c2d4bb864c3");}
MongoDB
一、基本使用
1.1业务应用场景
传统的关系型数据库(如Mysql),在数据库操作的“三高”需求以及对应web2.0的网站需求面前,显得力不从心
三高:
- High performance - 对数据库高并发读写的要求
- Huge Storage - 对海量数据的高效率存储和访问的需求
- High Scalability && High Avaliablity 对数据库的高可扩展性和高可用性的需求
具体的应用场景如:
- 社交场景,使用MongoDB存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
- 游戏场景,使用MongoDB存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
- 物流场景,使用MongoDB存储订单信息,订单状态在运送过程中会不断更新,以MongoDB内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
- 物联网场景,使用MongoDB存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
- 视频直播,使用MongoDB存储用户信息、点赞互动信息等。
这些应用场景中,数据操作方面的共同特点是:
- 数据量大
- 写入操作频繁(读写都很频繁)
- 价值较低的数据,对事务性要求不高
对于这样的数据,我们更适合使用MongoDB来实现数据的存储。什么时候选择MongoDB ?
在架构选型上,除了上述的三个特点外,如果你还犹豫是否要选择它?可以考虑以下的一些问题:应用不需要事务及复杂join支持
新应用,需求会变,数据模型无法确定,想快速迭代开发应用需要2000-3000以上的读写QPS(更高也可以)
应用需要TB甚至PB级别数据存储
那么我们什么时候选择 MongoDB 呢?
除了架构选型上, 除了上述三个特点之外, 还要考虑下面这些问题:
- 应用不需要事务及复杂 JOIN 支持
- 新应用, 需求会变, 数据模型无法确定, 想快速迭代开发
- 应用需要 2000 - 3000 以上的读写QPS(更高也可以)
- 应用需要 TB 甚至 PB 级别数据存储
- 应用发展迅速, 需要能快速水平扩展
- 应用要求存储的数据不丢失
- 应用需要
99.999%
高可用 - 应用需要大量的地理位置查询, 文本查询
如果上述有1个符合, 可以考虑 MongoDB, 2个及以上的符合, 选择 MongoDB 绝不会后悔.
如果用MySQL呢?
相对MySQL, 可以以更低的成本解决问题(包括学习, 开发, 运维等成本)
1.2 MongoDB 简介
MongoDB是一个开源, 高性能, 无模式的文档型数据库, 当初的设计就是用于简化开发和方便扩展, 是NoSQL数据库产品中的一种.是最 像关系型数据库(MySQL)的非关系型数据库. 它支持的数据结构非常松散, 是一种类似于 JSON 的 格式叫BSON, 所以它既可以存储比较复杂的数据类型, 又相当的灵活. MongoDB中的记录是一个文档, 它是一个由字段和值对(field:value)组成的数据结构.MongoDB文档类似于JSON对象, 即一个文档认 为就是一个对象.字段的数据类型是字符型, 它的值除了使用基本的一些类型外, 还可以包括其他文档, 普通数组和文档数组.
“最像关系型数据库的 NoSQL 数据库”. MongoDB 中的记录是一个文档, 是一个 key-value pair. 字段的数据类型是字符型, 值除了使用基本的一些类型以外, 还包括其它文档, 普通数组以及文档数组
MongoDB 数据模型是面向文档的, 所谓文档就是一种类似于 JSON 的结构, 简单理解 MongoDB 这个数据库中存在的是各种各样的 JSON(BSON)
- 数据库 (database)
- 数据库是一个仓库, 存储集合 (collection)
- 集合 (collection)
- 类似于数组, 在集合中存放文档
- 文档 (document)
- 文档型数据库的最小单位, 通常情况, 我们存储和操作的内容都是文档
在 MongoDB 中, 数据库和集合都不需要手动创建, 当我们创建文档时, 如果文档所在的集合或者数据库不存在, 则会自动创建数据库或者集合
1.3Windows下载
https://www.mongodb.com/try/download/community
1.4Linux下载
https://www.mongodb.com/try/download/community
1.5图形化界面
https://www.mongodb.com/try/download/compass
二、单机部署
三、基本使用
3.1案例需求
3.2数据库操作
- 设置MongoDB账号密码,防止被删库勒索
- 进入到mongodb服务器下
use admindb.createUser({user: '用户名', // 用户名(自定义)pwd: '密码', // 密码(自定义)roles:[{role: 'root', // 使用超级用户角色db: 'admin' // 指定数据库}]
})
- 找到配置的
mongodb.conf
文件,添加以下信息
security:authorization: enabled
- 重启mongodb服务
- 使用mongodb连接数据库,并使用超级管理员账号
在没有认证之前,也可以使用mongo链接数据库,但是不能执行其他命令
以下是两种使用超级管理员账号登录数据库的方式
// 方式一
mongo
use admin
db.auth('用户名', '密码')// 方式二
mongo admin -u 用户名 -p 密码
3.2.1权限说明(基于角色的权限控制)
3.2.1.1 内置角色
数据库用户角色
- read: 只读数据权限
- readWrite:写数据权限
数据库管理角色
- dbAdmin: 在当前db中执行管理操作的权限
- dbOwner: 在当前db中执行任意操作
- userADmin: 在当前db中管理user的权限
备份和还原角色
- backup
- restore
夸库角色
- readAnyDatabase: 在所有数据库上都有读取数据的权限
- readWriteAnyDatabase: 在所有数据库上都有读写数据的权限
- userAdminAnyDatabase: 在所有数据库上都有管理user的权限
- dbAdminAnyDatabase: 管理所有数据库的权限
集群管理
- clusterAdmin: 管理机器的最高权限
- clusterManager: 管理和监控集群的权限
- clusterMonitor: 监控集群的权限
- hostManager: 管理Server
超级权限
-
root: 超级用户
-
显示数据库
show dbs
3.2.1选择和创建数据库
选择和创建数据库
use 数据库名称
如果数据库不存在将会自动创建,比如将创建spitdb数据库
use spitdb
查看当前数据库
db
默认的三个数据库
- admin:从权限的角度看,这是root数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库权限。一些特定的服务器端命令也只能通过这个数据库运行,比如列出所有的数据库或者关闭服务器
- local:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
- config:当Mongo分片设置时,config数据库在内部使用,用于保存分片的相关信息
3.2.2数据库的删除
db.dropDatabase()
3.3集合操作
3.2.1集合的显示创建
db.createCollection(name) #name必须要由 "" 包围
查看当前库中的表
show tables
3.2.2集合的隐式创建
当向有一个集合中插入一个文档的时候,如果集合不存在,将会自动创建集合
3.2.3集合的删除
db.collection.drop()
或
db.集合name.drop()
返回值
如果成功删除选定的集合,则drop()方法返回true,否则返回false
例如:要删除mycollection集合
db.mycollection.drop()
3.4文档基本CRUD
文档(document)的数据结构和JSON基本一样
所有存储在集合中的数据都是JSON格式
3.4.1文档的插入
- 单个文档插入
使用insert() 或 save()方法向集合中插入文档,语法:
db.collection.insert(<document or array of documents>,{writeConcern: <document>,ordered: <boolean>}
)
参数
Paramter | Type | 描述 |
---|---|---|
document | document or array | 要插入到集合内的文档或文档数组(JSON格式) |
writeConcern | document | |
orderd | boolean | 可选。true代表按顺序插入数组中的文档,如果其中一个文档出现错误,MongoDB将返回而不处理数组中的其他文档;如果为false,则执行顺序插入,如果其中一个文档出现错误,则继续处理数组中的主文档。在2.6+版本,默认为true |
示例
db.comment.insert({"articleid":"100000","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}
)
提示
- comment如果不存在,将会被隐式创建
- mongodb中的数字,默认情况下时
double
类型,如果要存入整形,必须使用函数Number(整型数字)
,否则取出来会有问题 - 插入当前日期使用
new Date()
- 插入的数据没有指定
_id
,会自动生成主键值 - 如果某字段没值,可以赋值为null,或不写
插入成功后的返回值
WriteResult({ "nInserted" : 1 })
- 多个文档插入
db.collection.insert([<document or array of documents>,{writeConcern: <document>,ordered: <boolean>},{writeConcern: <document>,ordered: <boolean>},{writeConcern: <document>,ordered: <boolean>},....])
db.comment.insert([{"articleid":"100001","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100002","content":"今天天气真好","userId":"1002","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100003","content":"今天天气真好","userId":"1003","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100004","content":"今天天气真好","userId":"1004","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}
]);
如果担心多个数据插入查询失败,可以加上try-catch
try{db.comment.insert([{"articleid":"100001","content":"今天天气真好","userId":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100002","content":"今天天气真好","userId":"1002","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100003","content":"今天天气真好","userId":"1003","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null},{"articleid":"100004","content":"今天天气真好","userId":"1004","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null}]);
}catch(e){print(e);
}
3.4.2文档的查询
db.collection.find(<query>,[projection])
参数:
Parameter | Type | Description |
---|---|---|
query | document | 可选,使用查询运算符指定选择筛选器。若要返回集合中的所有文档,请省略此参数或传递空文档 |
projection | document | 可选。指定要在于查询结果筛选器匹配的文档中返回的字段(投影)。若要返回匹配文档中的所有字段,请省略此参数 |
实例
db.comment.find()
或
db.comment.find({})#查询所有
db.comment.find(){ "_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"), "articleid" : "100000", "content" : "今天天气真好", "userId" : "1001", "nickname" : "Rose", "createdatetime" : ISODate("2024-09-03T07:10:52.544Z"), "likenum" : 10, "state" : null }#查询指定内容
db.comment.find({userId:"1001"}){ "_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"), "articleid" : "100000", "content" : "今天天气真好", "userId" : "1001", "nickname" : "Rose", "createdatetime" : ISODate("2024-09-03T07:10:52.544Z"), "likenum" : 10, "state" : null }#格式
db.comment.findOne({userId:"1001"})
{"_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"),"articleid" : "100000","content" : "今天天气真好","userId" : "1001","nickname" : "Rose","createdatetime" : ISODate("2024-09-03T07:10:52.544Z"),"likenum" : 10,"state" : null
}#指定查询字段
db.comment.findOne({userId:"1001"},{articleid:1,content:1})
{"_id" : ObjectId("6312fdfc778ba7b2fb5d7bd6"),"articleid" : "100000","content" : "今天天气真好"
}#排除指定字段
db.comment.findOne({userId:"1001"},{articleid:1,content:1,_id:0})
{ "articleid" : "100000", "content" : "今天天气真好" }
3.4.3文档的更新
db.collection.update(query,update,options)
或
db.collection.uodate(<query>,<update>,{upsert: <boolean>,multi:<boolean>,writeConcern:<document>collation: [<filterdocument1>,...],hint:<document|string> }
)
示例
- 覆盖的修改
如果我们想修改id为1的记录,点赞量为1001,输入一下语句
db.comment.update({id:"1",{likenum:NumberInt(1001)})
执行后我们会发现,这条字段除了likenum,其他字段都不见了
- 局部修改
db.comment.update({id:"1",{{$set:likenum:NumberInt(1001)}})
- 批量修改
db.comment.update({id:"1",{{$set:likenum:NumberInt(1001)}},{multi:true})
如果不加参数,只修改第一条
- 列值增长的修改
如果我们想实现对某列值在原有值基础上进行增加或减少,可以使用$inc
运算符来实现
需求:对3号数据的点赞数,每次递增1
db.comment.update({id:"3",{{$inc:likenum:NumberInt(1)}})
3.4.4文档的删除
db.集合名称.remove(条件)
以下为将数据全部删除
db.comment.remove({})
如果删除_id=1的记录,输入以下:
db.comment.remove({_id:"1"})
3.5文档的分页查询
3.5.1统计查询
统计查询使用count()方法
db.collection.count(query,options)
示例:
db.comment.count(Userid:"1003")
3.5.2列表查询
使用limit()可以限制查询的条数,使用skip()方法来跳过指定数量的数据
db.collection.find(x)
skip()
db.comment.find().skip(x)
例子
- 查找前四条数据
db.comment.find().limit(4)
- 查找第2条到最后一条数据
db.comment.find.skip(2)
- 查找第2条到第4条数据
#跳过前两条记录,查询两条记录
db.comment.find().skip(2).limit(2)
3.5.5排序查询
sort()方法对数据进行排序,sort()方法可以通过参数指定排序的字段,并使用1和-1来指定排序的方式,其中1为升序排序,-1位降序排序
#升序
db.collection.find().sort({key:1})#降序
db.collection.find().sort({key:-1})
例如
对userId进行降序排序
db.comment.find({},{userid:1}),sort({key:-1})
多个条件时
db.comment.find({},{userId:1,likenum:1}).sort(userId:-1,likenum:1)
3.6文档的更多查询
3.6.1正则的复杂条件查询
MongoDB的模糊查询是通过正则表达式的方式实现的。格式为:
db.collection.find({field:/正则表达式/})
或
db.集合.find({字段:/正则表达式/})
提示:正则表达式是js的语法,直接量的写法
例如,我要查询评论内容包含“开水”的所有文档,代码如下
db.comment.find({content:/开水/})
如果要查询评论的内容中以“专家”开头的,代码如下
db.comment.find({content:/^专家/})
3.6.2比较查询
<,<=,>,>=这些操作符也是很常用的,格式如下
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}}) //不等于: field != value
例子:查询评论点赞数量大于700的记录
db.comment.find({likenum:{$gt:NumberInt(7000)}})
3.6.3包含查询
使用$in
**示例:**查询评论的集合中userId字段包含1003或1004的文档
db.comment.find({userId:{$in["1003","1004"]}})
不包含$nIn
示例:
db.comment.find({userId:{$nin:["1003","1004"]}})
3.6.5条件连接查询
我们如果需要查询同时满足两个以上条件,需要使用$and操作符
$and:[{},{}]
**示例:**查询评论集合中likenum大于等于700并且小于2000的文档:
db.comment.find({$and:[{likenum:{$gte:NumberInt(700)},{likenum:{$it:NumberInt(2000)}}}]})
如果两个以上的条件时或者的关系,我们使用$or操作符
$or:[{},{}]
**示例:**查询评论集合中userId为1003,或者点赞数小于1000的文档
db.comment.find({$or:[{userId:"1003"},{likenum:{$lt:1000}}]})
四、索引
4.1概述
索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
如果查询存在适当的索引,MongoDB可以使用该索引限制必须检查的文档数。
索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB丕可以使用索引中的排序返回排序结果。
官网文档: https://docs.mongodb.com/manual/indexes/了解:
MogoDB索引使用B树数据结构(确切的说是B-Tree,MySQL是B+Tree)
4.2索引的类型
4.2.1单字段索引
MongoDB支持在文档的单个字段上创建用户定义的升序/降序索引,称为单字段索引(Single Field lndex)。对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为MongoDB可以在任何方向上遍历索引。
4.2.2复合索引
MongoDB还支持多个字段的用户定义索引,即复合索引(Compound Index)。
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由{ userid: 1, score: -1}组成,则索引首先按userid正序排序,然后在每个userid的值内,再在按score倒序排序。
4.2.3其他索引
地理空间索引(Geospatial lndex)、文本索引(Text Indexes)、哈希索引 (Hashed lndexes)。
- 地理空间索引 (Geospatial lndex):为了支持对地理空间坐标数据的有效查询,MongoDB提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。
- 文本索引(Text Indexes):MongoDB提供了一种文本索引类型,支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如如"the"、“a””、“or”),而将集合中的词作为词干,只存储根词。
- 哈希索引 (Hashed lndexes):为了支持基于散列的分片,MongoDB提供了散列索引类型,他对字段的散列进行索引,这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询
4.3索引的管理操作
4.3.1索引的查看
返回一个集合中的所有索引的数组
db.collection.getIndex()
仅MongoDB3.0+支持
**例子:**查看comment集合中所有的索引情况
db.comment.getIndexes()
结果中显示的是默认_id
索引
4.3.2索引的创建
- 单个索引
db.collection.createIndex(keys,options)
- 复合索引
# 根据userId升序,nickname降序创建
db.comment.createIndex({userId:1,nickname:-1})
4.3.3索引的移除
- 指定索引的移除
db.collection.dropIndex(index)
示例:
db.comment.dropIndex({userId:1})
- 移除所有索引
db.collection.dropIndexes()
_id
是不能被删除的,可以删除其他字段的索引
4.4索引的使用
4.4.1执行计划
类似mysql的执行计划,用来分析性能,通常使用执行计划来查看情况
db.collection.find(query,options).explain(options)
**示例:**根据userId查询数据的情况
db.comment.find({userId:"1003"}).explain()
> db.comment.find({userId:"1003"}).explain()
{"explainVersion" : "1","queryPlanner" : {"namespace" : "my.comment","indexFilterSet" : false,"parsedQuery" : {"userId" : {"$eq" : "1003"}},"queryHash" : "B1777DBA","planCacheKey" : "A6C2ADF9","maxIndexedOrSolutionsReached" : false,"maxIndexedAndSolutionsReached" : false,"maxScansToExplodeReached" : false,"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" : [ ]},"command" : {"find" : "comment","filter" : {"userId" : "1003"},"$db" : "my"},"serverInfo" : {"host" : "VM-24-17-centos","port" : 27017,"version" : "5.0.11","gitVersion" : "d08c3c41c105cde798ca934e3ac3426ac11b57c3"},"serverParameters" : {"internalQueryFacetBufferSizeBytes" : 104857600,"internalQueryFacetMaxOutputDocSizeBytes" : 104857600,"internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600,"internalDocumentSourceGroupMaxMemoryBytes" : 104857600,"internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600,"internalQueryProhibitBlockingMergeOnMongoS" : 0,"internalQueryMaxAddToSetBytes" : 104857600,"internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600},"ok" : 1
}
MongoDB Compass查看
4.4.2涵盖的查询
当查询掉件和查询的投影仅包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存,这些覆盖的查询可以非常有效
db.comment.find({userId:"1003"},{userId,_id:0})
五、案例:文章评论
5.1需求分析
5.2表结构分析
数据库:articledb
字段名称 | 作用 | 字段类型 | 备注 |
---|---|---|---|
_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表示文章的置顶评论 |
5.3技术选型
以下为SpringBoot项目搭建
5.3.1mongodb-driver
java链接的,类似于JDBC
5.3.2SpringDataMongoDB
SpringData家族成员之一,底部封装了mongodb-driver
5.4搭建Java环境
- maven
<!-- Mongodb--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!-- Mongodb-->
- yaml
spring:data:mongodb:host: 101.43.254.109database: articledbport: 27017#有账号密码就写,没有就注掉username: "用户名"password: "密码"#因为配置过对所有数据库通用的账号,所以这里加上authentication-database: admin#也可以使用url连接#uri:
5.5编写实体类
- pojo
package com.example.pojo;import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
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.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:27*/@Data
@ToString
//类名是大写,这里是为了和数据库对应
@Document(collection = "comment")
//新增复合索引,最好还是在命令行的方式建
//@CompoundIndex(def = "{'userId':1,'nickname':-1}")
public class Comment implements Serializable {//主键 该属性的值会自动对应mongodb的主键字段“_id",如果该属性名就交”id“,则该注解可以省略,否则必须写@Idprivate String id;//该属性对应mongodb的字段的名称,如果一致就不用写@Field("content")//内容private String content;//发布日期private Date publishTime;//发布人Id@Indexed //添加一个单字段的索引private String userId;//昵称private String nickname;//评论人日期private LocalDateTime createDatetime;//点赞数private Integer likeNum;//回复数private Integer replyNum;//状态private String state;//上级IDprivate String parentId;//文章IDprivate String articleId;}
5.6CRUD
- dao
package com.example.demo.dao;import com.example.pojo.Comment;
import org.springframework.data.mongodb.repository.MongoRepository;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:58*/
public interface CommentRepository extends MongoRepository<Comment,String> {
}
- service
package com.example.demo.service;import com.example.demo.dao.CommentRepository;
import com.example.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 15:59*/
@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository ;/*** 保存* @param comment c* @return comment*/public Comment saveComment(Comment comment){return commentRepository.save(comment);}/*** 更新* @param comment c* @return comment*/public Comment updateComment(Comment comment){return commentRepository.save(comment);}/*** 删除* @param id id*/public void deleteCommentById(String id){commentRepository.deleteById(id);}/*** 查询所有* @return list*/public List<Comment> findCommentList(){return commentRepository.findAll();}/*** 根据id查询频率* @param id id* @return comment*/public Comment findCommentByID(String id){return commentRepository.findById(id).get();}
}
- Test
package com.example.demo.service;import com.example.pojo.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;/*** @author 我见青山多妩媚* @date Create on 2024/9/5 16:09*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class CommentServiceTest {@Autowiredprivate CommentService commentService;@Testpublic void findCommentList(){List<Comment> commentList = commentService.findCommentList();for (Comment comment : commentList) {System.out.println(comment);}}@Testpublic void save(){Comment comment = new Comment();comment.setArticleId("100002");comment.setContent("我不爱喝水");comment.setUserId("1005");comment.setNickname("海绵宝宝");comment.setCreateDatetime(LocalDateTime.now());comment.setLikeNum(6000);comment.setState("1");commentService.saveComment(comment);}@Testpublic void findById(){//Comment(id=6315bb3efabdb55ab0d8ab2f, content=我爱喝水, publishTime=null, userId=1004, nickname=杰克船长, createDatetime=2024-09-05T17:02:54.301, likeNum=2000, replyNum=null, state=1, parentId=null, articleId=100001)System.out.println(commentService.findCommentByID("6315bb3efabdb55ab0d8ab2f"));}
}
5.7根据上级ID查询文章评论的分页列表
- CommentRepository新增
Page<Comment> findByParentId(String parentId, Pageable pageable);
- CommentService新增
/*** 分页查询* @param parentId 父级id* @param page 页码* @param size 每页size* @return comment*/public Page<Comment> findCommentListByParentId(String parentId,int page,int size){return commentRepository.findByParentId(parentId, PageRequest.of(page-1, size));}
- Test
@Testpublic void findByParentId(){Page<Comment> page = commentService.findCommentListByParentId("10004", 1, 4);System.out.println("total:"+page.getTotalElements());List<Comment> content = page.getContent();System.out.println("all:"+page.getContent());for (Comment comment : content) {System.out.println(comment);}}
5.8MongoTemplate实现评论点赞
我们看以下节点的临时示例代码:CommentService新增updateThumbup方法
/*** 评论点赞* 效率低*/public void updateCommentThumbupToIncrementingOld(String id){Comment comment = commentRepository.findById(id).get();comment.setLikeNum(comment.getLikeNum()+1);commentRepository.save(comment);}
以上方法虽然实现起来比较简单,但是执行效率比较高,因为我们只需要将点赞数+1就可以了,没必要查询出所有字段修改后再更新所有字段
优化
我们可以使用MongoTemplate类来实现对某列的操作
- 修改CommentService
@Autowiredprivate MongoTemplate mongoTemplate;/*** 点赞优化* @param id id*/public void updateCommentLikeNum(String id){//查询条件 数据库里的id 等于传进来的id时Query query = Query.query(Criteria.where("_id").is(id));//更新条件Update update = new Update();//指定字段为likenum,步长为1update.inc("likeNum",1);//指定query、update、指定表<comment.class>mongoTemplate.updateFirst(query,update,Comment.class);}
- 测试
@Testpublic void updateCommentLikeNum(){commentService.updateCommentLikeNum("6315bfcb234e2c2d4bb864c3");}