• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

MongoDB基本操作+集成SpringBoot+案例

武飞扬头像
芝士汉堡 ིྀིྀ
帮助1

一、MongoDB介绍

1.基本概念

MongoDB是一个基于分布式文件存储的数据库。由C 语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。

MongoDB是一个介于关系数据库和非关系数据库(nosql)之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

2.概念解析

在mongodb中基本的概念是文档、集合、数据库,下面我们挨个介绍。下表将帮助您更容易理解Mongo中的一些概念:

SQL概念 MongoDB概念
数据库(databases) 数据库(database)
表(table) 集合(collection)
行(row) 文档(document)
列(column) 字段(field)
索引(indx) 索引(index)
主键(primary key) _id(字段)
视图(view) 视图(view)
表连接(table joins) 聚合操作($lookup)

二、MongoDB Shell使用

mongo是MongoDB的交互式JavaScript Shell界面,它为系统管理员提供了强大的界面,并为开发人员
提供了直接测试数据库查询和操作的方法。

mongo --port=27017
--port:指定端口,默认为27017
--host:连接的主机地址,默认127.0.0.1

2.1常用Shell命令

mongo shell是基于JavaScript语法的,MongoDB使用了SpiderMonkey作为其内部的JavaScript解释器引擎。

命令 说名
show dbs 显示数据库列表
use 数据库名 切换数据库,不存在则创建
db.dropDatabase() 删除数据库
show collections show tables
db.集合名.start() 查看集合详情
db.集合名.drop() 删除集合
show users 显示当前数据库的用户列表
show roles 显示当前数据的角色列表
show prifile 显示最近发生的操作
load(“xxx.js”) 执行一个js脚本
exit quit()
db.help() 查询当前数据库支持的方法
db.集合名.help() 显示集合的帮助信息
de.version() 查看数据库版本

2.2数据库操作

#查看所有库
show dbs
# 切换到指定数据库,不存在则创建
use test
# 删除当前数据库
db.dropDatabase()

2.3集合操作

#查看集合
show collections
#创建集合
db.createCollection("emp")
#删除集合
db.emp.drop()

创建集合语法

db.createCollection(name, options)

options参数

字段 类型 描述
capped 布尔 (可选)如果为true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。
size 数值 (可选)为固定集合指定一个最大值(以字节计)。如果 capped 为 true,也需要指定该字段。
max 数值 (可选)指定固定集合中包含文档的最大数量。

2.4安全认证

创建管理员账号

# 设置管理员用户名密码需要切换到admin库
use admin
#创建管理员
db.createUser({user:"fox",pwd:"fox",roles:["root"]})
# 查看所有用户信息
show users
#删除用户
db.dropUser("fox")

常用权限

权限名 描述
read 允许用户读取指定文件
readWrite 允许用户读写指定文件
dbAdmin 允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile
dbOwner 允许用户在指定数据库中执行任意操作,增、删、改、查等
userAdmin 允许用户向system.users集合写入,可以在指定数据库里创建、删除和管理用户
clusterAdmin 只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限
readAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的读权限
readWriteAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的读写权限
userAdminAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
dbAdminAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限
root 只在admin数据库中可用。超级账号,超级权限

2.5创建应用数据库用户

use appdb
db.createUser({user:"appdb",pwd:"fox",roles:["dbOwner"]})

默认情况下,MongoDB不会启用鉴权,以鉴权模式启动MongoDB

mongod -f /mongodb/conf/mongo.conf --auth

启用鉴权之后,连接MongoDB的相关操作都需要提供身份认证。

mongo 192.168.65.174:27017 -u fox -p fox --authenticationDatabase=admin

三、文档操作

3.1插入文档

3.2 版本之后新增了 db.collection.insertOne() 和db.collection.insertMany()。

3.1.1新增单个文档

db.collection.insertOne(
	<document>,
	{
		writeConcern: <document>
	}
)

writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:

  • 0:发起写操作,不关心是否成功;
  • 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
  • majority:写操作需要被复制到大多数节点上才算成功;

1、insert: 若插入的数据主键已经存在,则会抛 DuplicateKeyException 异常,提示主键重复,不保存当前数据。
2、save: 如果 _id 主键存在则更新数据,如果不存在就插入数据。

3.1.2批量新增文档

db.collection.insertMany(
	[ <document 1> , <document 2>, ... ],
	{
		writeConcern: <document>,
		ordered: <boolean>
	}
)

writeConcern:写入策略,默认为 1,即要求确认写操作,0 是不要求。
ordered:指定是否按顺序写入,默认 true,按顺序写入。

insert和save也可以实现批量插入

批量插入脚本

var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<50;i  ){
	var typeIdx = Math.floor(Math.random()*types.length);
	var tagIdx = Math.floor(Math.random()*tags.length);
	var favCount = Math.floor(Math.random()*100);
	var book = {
		title: "book-" i,
		type: types[typeIdx],
		tag: tags[tagIdx],
		favCount: favCount,
		author: "xxx" i
	};
	books.push(book)
}
db.books.insertMany(books);

进入mongo shell,执行

load("books.js")

3.2查询文档

find 查询集合中的若干文档。语法格式如下:

db.collection.find(query, projection)
  • query :可选,使用查询操作符指定查询条件
  • projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。投影时,id为1的时候,其他字必须是1;id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1。

findOne查询集合中的第一个文档。语法格式如下:

db.collection.findOne(query, projection)

3.2.1条件查询

#查询带有nosql标签的book文档:
db.books.find({tag:"nosql"})
#按照id查询单个book文档:
db.books.find({_id:ObjectId("61caa09ee0782536660494d9")})
#查询分类为“travel”、收藏数超过60个的book文档:
db.books.find({type:"travel",favCount:{$gt:60}})

查询条件对照表

SQL MQL
a = 1 {a: 1}
a <> 1 {a: {$ne: 1}}
a > 1 {a: {$gt: 1}}
a >= 1 {a: {$gte: 1}}
a < 1 {a: {$lt: 1}}
a <= 1 {a: {$lte: 1}}

查询逻辑对照表

SQL MQL
a = 1 AND b = 1 {a: 1, b: 1}或{$and: [{a: 1}, {b: 1}]}
a = 1 OR b = 1 {$or: [{a: 1}, {b: 1}]}
a IS NULL {a: {$exists: false}}
a IN (1, 2, 3) {a: {$in: [1, 2, 3]}}

查询逻辑运算符

  • $lt: 存在并小于
  • $lte: 存在并小于等于
  • $gt: 存在并大于
  • $gte: 存在并大于等于
  • $ne: 不存在或存在但不等于
  • $in: 存在并在指定数组中
  • $nin: 不存在或不在指定数组中
  • $or: 匹配两个或多个条件中的一个
  • $and: 匹配全部条件

3.2.2排序&分页

指定排序
在 MongoDB 中使用 sort() 方法对数据进行排序

#指定按收藏数(favCount)降序返回
db.books.find({type:"travel"}).sort({favCount:-1})

分页查询
skip用于指定跳过记录数,limit则用于限定返回结果数量。可以在执行find命令的同时指定skip、limit参数,以此实现分页的功能。比如,假定每页大小为8条,查询第3页的book文档:

db.books.find().skip(8).limit(4)

3.2.3正则匹配查询*

MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式

//使用正则表达式查找type包含 so 字符串的book
db.books.find({type:{$regex:"so"}})
//或者
db.books.find({type:/so/})

3.3更新文档

可以用update命令对指定的数据进行更新,命令的格式如下:

db.collection.update(query,update,options)
  • query:描述更新的查询条件;
  • update:描述更新的动作及新的内容;
  • options:描述更新的选项
    • upsert: 可选,如果不存在update的记录,是否插入新的记录。默认false,不插入
    • multi: 可选,是否按条件查询出的多条记录全部更新。 默认false,只更新找到的第一条记录
    • writeConcern :可选,决定一个写操作落到多少个节点上才算成功。

3.3.1更新操作符

操作符 格式 描述
$set {$set:{field:value}} 指定一个键并更新值,若键不存在则创建
$unset {$unset : {field : 1 }} 删除一个键
$inc {$inc : {field : value } } 对数值类型进行增减
$rename {$rename : {old_field_name :new_field_name } } 修改字段名称
$push { $push : {field : value } } 将数值追加到数组中,若数组不存在则会进行初始化
$pushAll {$pushAll : {field : value_array }} 追加多个值到一个数组字段内
$pull {$pull : {field : _value } } 从数组中删除指定的元素
$addToSet {$addToSet : {field : value } } 添加元素到数组中,具有排重功能
$pop {$pop : {field : 1 }} 删除数组的第一个或最后一个元素

3.3.2更新单个文档

某个book文档被收藏了,则需要将该文档的favCount字段自增

db.books.update({_id:ObjectId("61caa09ee0782536660494d9")},{$inc:{favCount:1}})

3.3.3更新多个文档

默认情况下,update命令只在更新第一个文档之后返回,如果需要更新多个文档,则可以使用multi选项。

将分类为“novel”的文档的增加发布时间(publishedDate)

db.books.update({type:"novel"},{$set:{publishedDate:new Date()}},{"multi":true})

multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新

update命令的选项配置较多,为了简化使用还可以使用一些快捷命令:

  • updateOne:更新单个文档。
  • updateMany:更新多个文档。
  • replaceOne:替换单个文档。

3.3.4使用upsert命令

upsert是一种特殊的更新,其表现为如果目标文档不存在,则执行插入命令

db.books.update(
	{title:"my book"},
	{$set:{tags:["nosql","mongodb"],type:"none",author:"fox"}},
	{upsert:true}
)

3.3.5实现replace语义

update命令中的更新描述(update)通常由操作符描述,如果更新描述中不包含任何操作符,那么MongoDB会实现文档的replace语义

db.books.update(
	{title:"my book"},
	{justTitle:"my first book"}
)

3.3.6findAndModify命令

findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档

//将某个book文档的收藏数(favCount)加1
db.books.findAndModify({
	query:{_id:ObjectId("61caa09ee0782536660494dd")},
	update:{$inc:{favCount:1}}
})

该操作会返回符合查询条件的文档数据,并完成对文档的修改。
默认情况下,findAndModify会返回修改前的“旧”数据。如果希望返回修改后的数据,则可以指定new选项

db.books.findAndModify({
	query:{_id:ObjectId("61caa09ee0782536660494dd")},
	update:{$inc:{favCount:1}},
	new: true
})

与findAndModify语义相近的命令如下:

  • findOneAndUpdate:更新单个文档并返回更新前(或更新后)的文档。
  • findOneAndReplace:替换单个文档并返回替换前(或替换后)的文档.

3.4删除文档

3.4.1使用 remove 删除文档

  • remove 命令需要配合查询条件使用;
  • 匹配查询条件的文档会被删除;
  • 指定一个空文档条件会删除所有文档;
db.user.remove({age:28})// 删除age 等于28的记录
db.user.remove({age:{$lt:25}}) // 删除age 小于25的记录
db.user.remove( { } ) // 删除所有记录
db.user.remove() //报错

remove命令会删除匹配条件的全部文档,如果希望明确限定只删除一个文档,则需要指定justOne参数,命令格式如下:

db.collection.remove(query,justOne)

例如:删除满足type:novel条件的首条记录

db.books.remove({type:"novel"},true)

3.4.2使用 delete 删除文档*

官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档,语法格式如下:

db.books.deleteMany ({}) //删除集合下全部文档
db.books.deleteMany ({ type:"novel" }) //删除 type等于 novel 的全部文档
db.books.deleteOne ({ type:"novel" }) //删除 type等于novel 的一个文档

注意: remove、deleteMany等命令需要对查询范围内的文档逐个删除,如果希望删除整个集合,则使用drop命令会更加高效

3.4.3返回被删除文档

remove、deleteOne等命令在删除文档后只会返回确认性的信息,如果希望获得被删除的文档,则可以使用findOneAndDelete命令

db.books.findOneAndDelete({type:"novel"})

四、聚合操作

聚合操作处理数据记录并返回计算结果(诸如统计平均值,求和等)。聚合操作组值来自多个文档,可以对分组数据执行各种操作以返回单个结果。聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。

  • 单一作用聚合:提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档。
  • 聚合管道是一个数据聚合的框架,模型基于数据处理流水线的概念。文档进入多级管道,将文档转换为聚合结果。
  • MapReduce操作具有两个阶段:处理每个文档并向每个输入文档发射一个或多个对象的map阶
    段,以及reduce组合map操作的输出阶段。

4.1单一聚和

MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.count(),db.collection.distinct() 这类单一作用的聚合函数。 所有这些操作都聚合来自单个集合的文档。虽然这些操作提供了对公共聚合过程的简单访问,但它们缺乏聚合管道和map-Reduce的灵活性和功能。

MongoDB聚合和Java Stream的功能有一些相似之处

函数 描述
db.collection.estimatedDocumentCount() 忽略查询条件,返回集合或视图中所有文档的计数
db.collection.count() 返回与find()集合或视图的查询匹配的文档计数 。等同于db.collection.find(query).count()构造
db.collection.distinct() 在单个集合或视图中查找指定字段的不同值,并在数组中返回结果。
#检索books集合中所有文档的计数
db.books.estimatedDocumentCount()
#计算与查询匹配的所有文档
db.books.count({favCount:{$gt:50}})
#返回不同type的数组
db.books.distinct("type")
#返回收藏数大于90的文档不同type的数组
db.books.distinct("type",{favCount:{$gt:90}})

注意:在分片群集上,如果存在孤立文档或正在进行块迁移,则db.collection.count()没有查询谓词可能导致计数不准确。要避免这些情况,请在分片群集上使用 db.collection.aggregate()方法。

4.2管道聚合

MongoDB聚合框架是MongoDB提供的一种数据处理方式,用于对数据进行聚合、处理和转化。它基于管道(Pipeline)的概念,以一种类似于函数式编程的方式对数据进行流式处理。

聚合框架可以完成比传统查询更复杂的操作,并将多个操作集成到一个命令中,大大提高了查询效率和灵活性。在聚合框架中,可以使用诸如 m a t c h 、 match、 matchgroup、 s o r t 、 sort、 sortproject、 l o o k u p 、 lookup、 lookupunwind等众多操作符进行数据处理,甚至还可以编写JavaScript脚本来实现更加复杂的计算和操作。

使用聚合框架,可以对MongoDB中的数据进行分组、过滤、排序、计算、连接等操作,并且支持对聚合操作的结果进行分页、去重、限制等操作。

4.2.1管道(Pipeline)和阶段(Stage)

整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的, 每个管道:

  • 接受一系列文档(原始数据);
  • 每个阶段对这些文档进行一系列运算;
  • 结果文档输出给下一个阶段;

聚合管道操作语法

pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
  • pipelines 一组数据聚合阶段。除 o u t 、 out、 outMerge和$geonear阶段之外,每个阶段都可以在管道中出现多次。
  • options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时间、读写策略、强制索引等等

常用的管道聚合阶段

阶段 描述 SQL等价运算符
$match 筛选条件 WHERE
$project 投影 AS
$lookup 左外连接 LEFT OUTER JOIN
$sort 排序 ORDER BY
$group 分组 GROUP BY
$skip/$limit 分页  
$unwind 展开数组  
$graphLookup 图搜索  
$facet/$bucket 分面搜索  
准备数据集,执行脚本
var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<50;i  ){
	var typeIdx = Math.floor(Math.random()*types.length);
	var tagIdx = Math.floor(Math.random()*tags.length);
	var tagIdx2 = Math.floor(Math.random()*tags.length);
	var favCount = Math.floor(Math.random()*100);
	var username = "xx00" Math.floor(Math.random()*10);
	var age = 20   Math.floor(Math.random()*15);
	var book = {
		title: "book-" i,
		type: types[typeIdx],
		tag: [tags[tagIdx],tags[tagIdx2]],
		favCount: favCount,
		author: {name:username,age:age}
};
	books.push(book)
}
db.books.insertMany(books);
$project

投影操作, 将原始字段投影成指定名称, 如将集合中的 title 投影成 name

db.books.aggregate([{$project:{name:"$title"}}])

$project 可以灵活控制输出文档的格式,也可以剔除不需要的字段

db.books.aggregate([{$project:{name:"$title",_id:0,type:1,author:1}}])

从嵌套文档中排除字段

db.books.aggregate([
	{$project:{name:"$title",_id:0,type:1,"author.name":1}}
])
或者
db.books.aggregate([
	{$project:{name:"$title",_id:0,type:1,author:{name:1}}}
])
$match

$match用于对文档进行筛选,之后可以在得到的文档子集上做聚合,$match可以使用除了地理空间之外的所有常规查询操作符,在实际应用中尽可能将$match放在管道的前面位置。这样有两个好处:一是可以快速将不需要的文档过滤掉,以减少管道的工作量;二是如果再投射和分组之前执行$match,查询可以使用索引。

db.books.aggregate([{$match:{type:"technology"}}])

筛选管道操作和其他管道操作配合时候时,尽量放到开始阶段,这样可以减少后续管道操作符要操作的文档数,提升效率

$count

计数并返回与查询匹配的结果数

db.books.aggregate([ {$match:{type:"technology"}}, {$count: "type_count"}
])

$match阶段筛选出type匹配technology的文档,并传到下一阶段;
$count阶段返回聚合管道中剩余文档的计数,并将该值分配给type_count

$group

按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id字段,该字段按键包含不同的组。

输出文档还可以包含计算字段,该字段保存由$group的_id字段分组的一些accumulator表达式的值。$group不会输出具体的文档而只是统计信息。

{ $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ...
} }
  • id字段是必填的;但是,可以指定id值为null来为整个输入文档计算累计值。
  • 剩余的计算字段是可选的,并使用运算符进行计算。
  • _id和表达式可以接受任何有效的表达式。
    accumulator操作符
名称 描述 类比sql
$avg 计算均值 avg
$first 返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。 limit0,1
$last 返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。 -
$max 根据分组,获取集合中所有文档对应值得最大值。 max
$min 根据分组,获取集合中所有文档对应值得最小值。 min
$push 将指定的表达式的值添加到一个数组中。 -
$addToSet 将表达式的值添加到一个集合中(无重复值,无序)。 -
$sum 计算总和 sum
$stdDevPop 返回输入值的总体标准偏差(population standard deviation) -
$stdDevSamp 返回输入值的样本标准偏差(the sample standard deviation) -

$group阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group操作以写入临时文件。

book的数量,收藏总数和平均值
db.books.aggregate([
	{$group:{_id:null,count:{$sum:1},pop:{$sum:"$favCount"},avg:
	{$avg:"$favCount"}}}
])

统计每个作者的book收藏总数.
db.books.aggregate([
	{$group:{_id:"$author.name",pop:{$sum:"$favCount"}}}
])

统计每个作者的每本book的收藏数
db.books.aggregate([
	{$group:{_id:{name:"$author.name",title:"$title"},pop:{$sum:"$favCount"}}}
])

每个作者的book的type合集
db.books.aggregate([
	{$group:{_id:"$author.name",types:{$addToSet:"$type"}}}
])
$unwind

可以将数组拆分为单独的文档
v3.2 支持如下语法:

{
	$unwind:
	{
	 #要指定字段路径,在字段名称前加上$符并用引号括起来。
 	 path: <field path>,
	 #可选,一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。
	 includeArrayIndex: <string>,
	 #可选,default :false,若为true,如果路径为空,缺少或为空数组,则		$unwind输出文档
	 preserveNullAndEmptyArrays: <boolean>
} }
姓名为xx006的作者的book的tag数组拆分为多个文档
db.books.aggregate([
	{$match:{"author.name":"xx006"}},
	{$unwind:"$tag"}
])

每个作者的book的tag合集
db.books.aggregate([
	{$unwind:"$tag"},
	{$group:{_id:"$author.name",types:{$addToSet:"$tag"}}}
])
$limit

限制传递到管道中下一阶段的文档数

db.books.aggregate([
	{$limit : 5 }
])

此操作仅返回管道传递给它的前5个文档。 $limit对其传递的文档内容没有影响。

注意:当 s o r t 在管道中的 sort在管道中的 sort在管道中的limit之前立即出现时,$sort操作只会在过程中维持前n个结果,其中n是指定的限制,而MongoDB只需要将n个项存储在内存中

$skip

跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段

db.books.aggregate([
	{$skip : 5 }
])

此操作将跳过管道传递给它的前5个文档。 $skip对沿着管道传递的文档的内容没有影响

$sort

对所有输入文档进行排序,并按排序顺序将它们返回到管道。
语法:

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

要对字段进行排序,请将排序顺序设置为1或-1,以分别指定升序或降序排序,如下例所示:

db.books.aggregate([
	{$sort : {favCount:-1,title:1}}
])
$lookup

Mongodb 3.2版本新增,主要用来实现多表关联查询, 相当关系型数据库中多表关联查询。每个输入待处理的文档,经过$lookup 阶段的处理,输出的新文档中会包含一个新生成的数组(可根据需要命名新key )。数组列存放的数据是来自被Join集合的适配文档,如果没有,集合为空(即 为[ ])
语法:

db.collection.aggregate([{
	$lookup: {
		from: "<collection to join>",
		localField: "<field from the input documents>",
		foreignField: "<field from the documents of the from collection>",
		as: "<output array field>"
	}
})
属性 作用
from 同一个数据库下等待被Join的集合。
localField 源集合中的match值,如果输入的集合中,某文档没有 localField这个Key(Field),在处理的过程中,会默认为此文档含有 localField:null的键值对。
foreignField 待Join的集合的match值,如果待Join的集合中,文档没有foreignField值,在处理的过程中,会默认为此文档含有 foreignField:null的键值对。
as 为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉

注意:null = null 此为真
其语法功能类似于下面的伪SQL语句:

SELECT *, <output array field>
FROM collection
	WHERE <output array field> IN (SELECT *
									FROM <collection to join>
									WHERE <foreignField>= <collection.localField>);

4.2.2聚合操作示例1

统计每个分类的book文档数量
db.books.aggregate([
	{$group:{_id:"$type",total:{$sum:1}}},
	{$sort:{total:-1}}
])
标签的热度排行,标签的热度则按其关联book文档的收藏数(favCount)来计算
db.books.aggregate([
	{$match:{favCount:{$gt:0}}},
	{$unwind:"$tag"},
	{$group:{_id:"$tag",total:{$sum:"$favCount"}}},
	{$sort:{total:-1}}
])

1. $match阶段:用于过滤favCount=0的文档。
2. $unwind阶段:用于将标签数组进行展开,这样一个包含3个标签的文档会被拆解为3个条目。
3. $group阶段:对拆解后的文档进行分组计算,$sum:"$favCount"表示按favCount字段进行累
加。
4. $sort阶段:接收分组计算的输出,按total得分进行排序。
统计book文档收藏数[0,10),[10,60),[60,80),[80,100),[100, ∞)
db.books.aggregate([{
	$bucket:{
		groupBy:"$favCount",
		boundaries:[0,10,60,80,100],
		default:"other",
		output:{"count":{$sum:1}}
	}
}])

4.3MapReduce

MapReduce操作将大量的数据处理工作拆分成多个线程并行处理,然后将结果合并在一起。MongoDB提供的Map-Reduce非常灵活,对于大规模数据分析也相当实用。
MapReduce具有两个阶段:

  1. 将具有相同Key的文档数据整合在一起的map阶段
  2. 组合map操作的结果进行统计输出的reduce阶段
db.collection.mapReduce(
	function() {emit(key,value);}, //map 函数
	function(key,values) {return reduceFunction}, //reduce 函数
	{
		out: <collection>,
		query: <document>,
		sort: <document>,
		limit: <number>,
		finalize: <function>,
		scope: <document>,
		jsMode: <boolean>,
		verbose: <boolean>,
		bypassDocumentValidation: <boolean>
	}
)
  • map,将数据拆分成键值对,交给reduce函数
  • reduce,根据键将值做统计运算
  • out,可选,将结果汇入指定表
  • quey,可选筛选数据的条件,筛选的数据送入map
  • sort,排序完后,送入map
  • limit,限制送入map的文档数
  • finalize,可选,修改reduce的结果后进行输出
  • scope,可选,指定map、reduce、finalize的全局变量
  • jsMode,可选,默认false。在mapreduce过程中是否将数 据转换成bson格式。
  • verbose,可选,是否在结果中显示时间,默认false
  • bypassDocmentValidation,可选,是否略过数据校验
    统计type为travel的不同作者的book文档收藏数
db.books.mapReduce(
	function(){emit(this.author.name,this.favCount)},
	function(key,values){return Array.sum(values)},
	{
		query:{type:"travel"},
		out: "books_favCount"
	}
)

五、集成SpringBoot

5.1环境搭建

1.引入依赖

<!--spring data mongodb-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

2.配置yml

  data:
    mongodb:
      host: localhost
      database: applet
      port: 27017
#      authentication-database:
#      username:
#      password:

3.使用时注入mongoTemplate

@Autowired
MongoTemplate mongoTemplate;

5.2集合操作

@Test
public void testCollection(){
	boolean exists = mongoTemplate.collectionExists("emp");
	if (exists) {
		//删除集合
		mongoTemplate.dropCollection("emp");
	}
	//创建集合
	mongoTemplate.createCollection("emp");
}

5.3文档操作

5.3.1相关注解

  • @Document
    • 修饰范围: 用在类上
    • 作用: 用来映射这个类的一个对象为mongo中一条文档数据。
    • 属性:( value 、collection )用来指定操作的集合名称
  • @Id
    • 修饰范围: 用在成员变量、方法上
    • 作用: 用来将成员变量的值映射为文档的_id的值
  • @Field
    • 修饰范围: 用在成员变量、方法上
    • 作用: 用来将成员变量及其值映射为文档中一个key:value对。
    • 属性:( name , value )用来指定在文档中 key的名称,默认为成员变量名
  • @Transient
    • 修饰范围:用在成员变量、方法上
    • 作用:用来指定此成员变量不参与文档的序列化

5.3.2创建实体

@Document("emp") //对应emp集合中的一个文档
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
	@Id //映射文档中的_id
	private Integer id;
	@Field("username")
	private String name;
	@Field
	private int age;
	@Field
	private Double salary;
	@Field
	private Date birthday;
}

5.3.3添加文档

insert方法返回值是新增的Document对象,里面包含了新增后id的值。如果集合不存在会自动创建集合。通过Spring Data MongoDB还会给集合中多加一个class的属性,存储新增时Document对应Java中类的全限定路径。这么做为了查询时能把Document转换为Java类型。

@Test
public void testInsert(){
	Employee employee = new Employee(1, "小明", 30,10000.00, new Date());
	//添加文档
	// sava: _id存在时更新数据
	//mongoTemplate.save(employee);
	// insert: _id存在抛出异常 支持批量操作
	mongoTemplate.insert(employee);
	List<Employee> list = Arrays.asList(
	new Employee(2, "张三", 21,5000.00, new Date()),
	new Employee(3, "李四", 26,8000.00, new Date()),
	new Employee(4, "王五",22, 8000.00, new Date()),
	new Employee(5, "张龙",28, 6000.00, new Date()),
	new Employee(6, "赵虎",24, 7000.00, new Date()),
	new Employee(7, "赵六",28, 12000.00, new Date()));
	//插入多条数据
	mongoTemplate.insert(list,Employee.class);
}

5.3.4查询文档

Criteria是标准查询的接口,可以引用静态的Criteria.where的把多个条件组合在一起,就可以轻松地将多个方法标准和查询连接起来,方便我们操作查询语句。

Criteria Mongodb 说明
Criteria and(String key) $and 并且
Criteria andOperator(Criteria…criteria) $and 并且
Criteria orOperator(Criteria…criteria) $or 或者
Criteria gt(Object o) $gt 大于
Criteria gte(Object o) $gte 大于等于
Criteria in (Object…o) $in 包含
Criteria is (Object o) $is 等于
Criteria It (Object o) $It 小于
Criteria Ite (Object o) $Ite 小等于
Criteria nin (Object…o) $nin 不包含
@Test
public void testFind(){
	System.out.println("==========查询所有文档===========");
	//查询所有文档
	List<Employee> list = mongoTemplate.findAll(Employee.class);
	list.forEach(System.out::println);
	
	System.out.println("==========根据_id查询===========");
	//根据_id查询
	Employee e = mongoTemplate.findById(1, Employee.class);
	System.out.println(e);
	
	System.out.println("==========findOne返回第一个文档===========");
	//如果查询结果是多个,返回其中第一个文档对象
	Employee one = mongoTemplate.findOne(new Query(), 		Employee.class);
	System.out.println(one);

	System.out.println("==========条件查询===========");
	//new Query() 表示没有条件
	//查询薪资大于等于8000的员工
	//Query query = new Query(Criteria.where("salary").gte(8000));
	
	//查询薪资大于4000小于10000的员工
	//Query query = new Query(Criteria.where("salary").gt(4000).lt(10000));
	
	//正则查询(模糊查询) java中正则不需要有//
	//Query query = new Query(Criteria.where("name").regex("张"));

	//and or 多条件查询
	Criteria criteria = new Criteria();
	//and 查询年龄大于25&薪资大于8000的员工
	//criteria.andOperator(Criteria.where("age").gt(25),Criteria.where("salary").gt(8000));
	//or 查询姓名是张三或者薪资大于8000的员工
	criteria.orOperator(Criteria.where("name").is("张三"),Criteria.where("salary").gt(5000));
	Query query = new Query(criteria);
	
	//sort排序
	//query.with(Sort.by(Sort.Order.desc("salary")));
	//skip limit 分页 skip用于指定跳过记录数,limit则用于限定返回结果数量。
	query.with(Sort.by(Sort.Order.desc("salary")))
			.skip(0) //指定跳过记录数
			.limit(4); //每页显示记录数
			
	//查询结果
	List<Employee> employees = mongoTemplate.find(
		query, Employee.class);
	employees.forEach(System.out::println);
}
@Test
public void testFindByJson() {
	//使用json字符串方式查询
	//等值查询
	//String json = "{name:'张三'}";
	//多条件查询
	String json = "{$or:[{age:{$gt:25}},{salary:{$gte:8000}}]}";
	Query query = new BasicQuery(json);
	//查询结果
	List<Employee> employees = mongoTemplate.find(
	query, Employee.class);
	employees.forEach(System.out::println);
}

5.3.5更新文档

在Mongodb中无论是使用客户端API还是使用Spring Data,更新返回结果一定是受行数影响。如果更新后的结果和更新前的结果是相同,返回0。

  • updateFirst() 只更新满足条件的第一条记录
  • updateMulti() 更新所有满足条件的记录
  • upsert() 没有符合条件的记录则插入数据
@Test
public void testUpdate(){
	//query设置查询条件
	Query query = new Query(Criteria.where("salary").gte(15000));
	
	System.out.println("==========更新前===========");
	List<Employee> employees = mongoTemplate.find(query, Employee.class);
	employees.forEach(System.out::println);

	Update update = new Update();
	//设置更新属性
	update.set("salary",13000);
	//updateFirst() 只更新满足条件的第一条记录
	//UpdateResult updateResult = mongoTemplate.updateFirst(query, update,
	Employee.class);
	//updateMulti() 更新所有满足条件的记录
	//UpdateResult updateResult = mongoTemplate.updateMulti(query, update,
	Employee.class);
	//upsert() 没有符合条件的记录则插入数据
	//update.setOnInsert("id",11); //指定_id
	UpdateResult updateResult = mongoTemplate.upsert(query, update,Employee.class);
	//返回修改的记录数
	System.out.println(updateResult.getModifiedCount());

	System.out.println("==========更新后===========");
	employees = mongoTemplate.find(query, Employee.class);
	employees.forEach(System.out::println);
}

5.3.6删除文档

@Test
public void testDelete(){
	//删除所有文档
	//mongoTemplate.remove(new Query(),Employee.class);
	
	//条件删除
	Query query = new Query(Criteria.where("salary").gte(10000));
	mongoTemplate.remove(query,Employee.class);
}

5.4聚合操作

MongoTemplate提供了aggregate方法来实现对数据的聚合操作。

基于聚合管道mongodb提供的可操作的内容:

支持的操作 java 接口说明
$project Aggregation.project 修改输入文档的结构。
$match Aggregation.match 用于过滤数据
$limit Aggregation.limit 用来限制MongoDB聚合管道返回的文档数
$skip Aggregation.skip 在聚合管道中跳过指定数量的文档
$unwind Aggregation.unwind 将文档中的某一个数组类型字段拆分成多条
$group Aggregation.group 将集合中的文档分组,可用于统计结果
$sort Aggregation.sort 将输入文档排序后输出
$geoNear Aggregation.geoNear 输出接近某一地理位置的有序文档

基于聚合操作Aggregation.group,mongodb提供可选的表达式

聚合表达式 java 接口说明
$sum Aggregation.group().sum(“field”).as(“sum”) 求和
$avg Aggregation.group().avg(“field”).as(“avg”) 求平均
$min Aggregation.group().min(“field”).as(“min”) 获取集合中所有文档对应值得最小值
$max Aggregation.group().max(“field”).as(“max”) 获取集合中所有文档对应值得最大值
$push Aggregation.group().push(“field”).as(“push”) 在结果文档中插入值到一个数组中
$addToSet Aggregation.group().addToSet(“field”).as(“addToSet”) 在结果文档中插入值到一个数组中,但不创建副本
$first Aggregation.group().first(“field”).as(“first”) 根据资源文档的排序获取第一个文档数据

示例:以聚合管道示例2为例
实体结构

@Document("zips")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Zips {
	@Id //映射文档中的_id
	private String id;
	@Field
	private String city;
	@Field
	private Double[] loc;
	@Field
	private Integer pop;
	@Field
	private String state;
}

返回人口超过1000万的州java实现

db.zips.aggregate( [
	{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
	{ $match: { totalPop: { $gt: 10*1000*1000 } } }
] )

@Test
public void test(){
	//$group
	GroupOperation groupOperation =Aggregation.group("state").sum("pop").as("totalPop");
	
	//$match
	MatchOperation matchOperation = Aggregation.match(Criteria.where("totalPop").gte(10*1000*1000));
	
	// 按顺序组合每一个聚合步骤
	TypedAggregation<Zips> typedAggregation =Aggregation.newAggregation(Zips.class,groupOperation, matchOperation);
	
	//执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据
	AggregationResults<Map> aggregationResults =mongoTemplate.aggregate(typedAggregation, Map.class);
	// 取出最终结果
	
	List<Map> mappedResults = aggregationResults.getMappedResults();
	for(Map map:mappedResults){
		System.out.println(map);
	}
}

返回各州平均城市人口

db.zips.aggregate( [
	{ $group: { _id: { state: "$state", city: "$city" }, cityPop: { $sum: "$pop" }
} },
	{ $group: { _id: "$_id.state", avgCityPop: { $avg: "$cityPop" } } },
	{ $sort:{avgCityPop:-1}}
] )


@Test
public void test2(){
	//$group
	GroupOperation groupOperation =Aggregation.group("state","city").sum("pop").as("cityPop");
	
	//$group
	GroupOperation groupOperation2 =Aggregation.group("_id.state").avg("cityPop").as("avgCityPop");

	//$sort
	SortOperation sortOperation =Aggregation.sort(Sort.Direction.DESC,"avgCityPop");

	// 按顺序组合每一个聚合步骤
	TypedAggregation<Zips> typedAggregation =Aggregation.newAggregation(Zips.class,groupOperation, groupOperation2,sortOperation);

	//执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据
	AggregationResults<Map> aggregationResults =mongoTemplate.aggregate(typedAggregation, Map.class);

	// 取出最终结果
	List<Map> mappedResults = aggregationResults.getMappedResults();
	for(Map map:mappedResults){
		System.out.println(map);
	}
}

六、实战案例

Ps:某应用小程序的消息通知模块,需要统计用户针对消息的查看情况(消息是否已读等)做统计,方便后续业务的开发。

考虑上述场景,如果使用MySQL存储用户消息,在table中设个字段来判断消息的状态,也可以完成上述业务需求,倘若后续客户量达到一定数量(几十万),当用户集中在某个时间段,几十万个用户均需要加载消息表,对mysql的压力不言而喻。而MongoDB在查询大型数据集时,查询效率优于MySQL。所以针对这个场景选择MongoDB较为合适。

6.1环境配置

见上述集成SpringBoot的配置

6.2编码

6.2.1message实体

/**
 * 记录的是消息,里面有接受者ID
 */
@Data
@Document(collection = "message")
public class MessageEntity implements Serializable {
    @Id
    private String _id;

    @Indexed(unique = true)
    private String uuid;

    @Indexed
    private Integer senderId;

    private String senderPhoto="https://static-1258386385.cos.ap-beijing.myqcloud.com/img/System.jpg";

    private String senderName;

    private String msg;

    @Indexed
    private Date sendTime;
}

6.2.2message实体操作

@Repository
public class MessageDao {

    @Resource
    private MongoTemplate mongoTemplate;

    /**
     * 保存消息
     *
     * @param entity
     * @return
     */
    public String insert(MessageEntity entity) {
        Date sendTime = entity.getSendTime();
        sendTime = DateUtil.offset(sendTime, DateField.HOUR, 8);
        entity.setSendTime(sendTime);
        entity = mongoTemplate.save(entity);
        return entity.get_id();
    }

    /**
     * 查询分页数据
     *
     * @param userId
     * @param start
     * @param length
     * @return
     */
    public List<HashMap> searchMessageByPage(int userId, long start, int length) {
        JSONObject json = new JSONObject()
                .set("$toString", "$_id");
        Aggregation aggregation = Aggregation.newAggregation( // 1.先根据条件查询
                Aggregation.addFields().addField("id").withValue(json).build(), // 2.添加id字段
                Aggregation.lookup("message_ref", "id", "messageId", "ref"), // 3.关联查询
                Aggregation.match(Criteria.where("ref.receiverId").is(userId)), // 4.匹配条件
                Aggregation.sort(Sort.by(Sort.Direction.DESC, "sendTime")), // 5.排序
                Aggregation.skip(start), // 6.跳过
                Aggregation.limit(length) // 7.限制
        );
        AggregationResults<HashMap> result = mongoTemplate.aggregate(aggregation, "message", HashMap.class);
        List<HashMap> list = result.getMappedResults();
        list.forEach(one -> {
            List<MessageRefEntity> refList = (List<MessageRefEntity>) one.get("ref");
            MessageRefEntity entity = refList.get(0);
            boolean readFlag = entity.getReadFlag(); // 读取状态
            String refId = entity.get_id();
            one.put("readFlag", readFlag);
            one.put("refId", refId);
            one.remove("ref");
            one.remove("_id");
            Date sendTime = (Date) one.get("sendTime"); // 8.转换时间
            sendTime = DateUtil.offset(sendTime, DateField.HOUR, -8); // 时区转换 转为北京时间

            String today = DateUtil.today();
            if (today.equals(DateUtil.date(sendTime).toDateStr())) {
                one.put("sendTime", DateUtil.format(sendTime, "HH:mm"));
            } else {
                one.put("sendTime", DateUtil.format(sendTime, "yyyy/MM/dd"));
            }
        });

        return list;
    }

    /**
     * @param id
     * @return
     * @description: 根据id查询消息
     */
    public HashMap searchMessageById(String id) {
        HashMap map = mongoTemplate.findById(id, HashMap.class, "message");
        Date sendTime = (Date) map.get("sendTime");
        sendTime = DateUtil.offset(sendTime, DateField.HOUR, -8);
        map.replace("sendTime", DateUtil.format(sendTime, "yyyy-MM-dd HH:mm"));
        return map;
    }
}

6.2.3MessageRefEntity 实体

/**
 * 记录接收人和已读状态
 */
@Data
@Document(collection = "message_ref")
public class MessageRefEntity implements Serializable {
    @Id
    private String _id;

    @Indexed
    private String messageId;

    @Indexed
    private Integer receiverId;

    @Indexed
    private Boolean readFlag;

    @Indexed
    private Boolean lastFlag;
}

6.2.4MessageRefEntity 实体操作

@Repository
public class MessageRefDao {
    @Resource
    private MongoTemplate mongoTemplate;

    /**
     * 保存消息
     *
     * @param entity
     * @return
     */
    public String insert(MessageRefEntity entity) {
        entity = mongoTemplate.save(entity);
        return entity.get_id();
    }

    /**
     * 查询未读消息的数量
     *
     * @param userId
     * @return
     */
    public long searchUnreadCount(int userId) {
        Query query = new Query().addCriteria(Criteria
                .where("receiverId")
                .is(userId)
                .and("readFlag")
                .is(false));
        return mongoTemplate.count(query, MessageRefEntity.class);
    }

    /**
     *查询接收到最新消息的数量
     * @param userId
     * @return
     */
    public long searchLastCount(int userId) {
        Query query = new Query().addCriteria(Criteria
                .where("lastFlag")
                .is(true)
                .and("receiverId")
                .is(userId));
        Update update = new Update().set("lastFlag", false); // 1.将最后一条消息标记为false
        UpdateResult result = mongoTemplate.updateMulti(query, update, "message_ref"); // 2.更新
        long rows = result.getModifiedCount(); // 3.获取更新的行数
        return rows;
    }


    /**
     * 更新消息状态将ref消息未读变成已读
     * @param id
     * @return
     */
    public long updateUnreadMessage(String id) {
        Query query = new Query().addCriteria(Criteria
                .where("_id")
                .is(id));
        Update update = new Update().set("readFlag", true);
        UpdateResult result = mongoTemplate.updateFirst(query, update, "message_ref");
        long rows = result.getModifiedCount();
        return rows;
    }

    //根据主键值删除消息记录
    public long deleteMessageRefById(String id) {
        Query query = new Query().addCriteria(Criteria
                .where("_id")
                .is(id));
        DeleteResult result = mongoTemplate.remove(query, "message_ref");
        long rows = result.getDeletedCount();
        return rows;
    }


    /**
     * 根据userId删除用户所有的消息
     * @param userId
     * @return
     */
    public long deleteUserMessageRef(int userId) {
        Query query = new Query().addCriteria(Criteria
                .where("receiverId")
                .is(userId));
        DeleteResult result = mongoTemplate.remove(query, "message_ref");
        long rows = result.getDeletedCount();
        return rows;
    }
}

6.2.5消息模块接口

@RestController
@RequestMapping("/message")
@Api(tags = "消息模块接口")
public class MessageController {
    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private MessageService messageservice;

    @Resource
    private MessageTask messageTask;

    @GetMapping("/searchMessageByPage")
    @ApiOperation(value = "获取分页消息列表")
    public R searchMessageByPage(@Valid @RequestBody SearchMessageByPageVO searchMessageByPageVO, @RequestHeader String token) {
        int userId = jwtUtil.getUserId(token);
        /*计算分页偏移量*/
        int offset = (searchMessageByPageVO.getPage() - 1) * searchMessageByPageVO.getLength();
        List<HashMap> list = messageservice.searchMessageByPage(userId, offset, searchMessageByPageVO.getLength());
        return R.ok().put("result", list);
    }

    @PostMapping("/searchMessageById")
    @ApiOperation("根据ID查询消息")
    public R searchMessageById(@Valid @RequestBody SearchMessageByIdVO searchMessageByIdVO) {
        HashMap map = messageservice.searchMessageById(searchMessageByIdVO.getId());
        return R.ok().put("result", map);
    }

    @PostMapping("/updateUnreadMessage")
    @ApiOperation("未读消息更新成已读消息")
    public R updateUnreadMessage(@Valid @RequestBody UpdateUnreadMessageVO updateUnreadMessageVO) {
        long rows = messageservice.updateUnreadMessage(updateUnreadMessageVO.getId());
        return R.ok().put("result", rows == 1 ? true : false);
    }

    @PostMapping("/deleteMessageRefById")
    @ApiOperation("删除消息")
    public R deleteMessage(@Valid @RequestBody SearchMessageByIdVO searchMessageByIdVO) {
        long rows = messageservice.deleteMessageRefById(searchMessageByIdVO.getId());
        return R.ok().put("result", rows == 1 ? true : false);
    }

    @GetMapping("/refreshMessage")
    @ApiOperation("刷新用户消息")
    public R refreshMessage(@RequestHeader String token) {
        int userId = jwtUtil.getUserId(token);
        // 异步接收消息
        messageTask.receiveAsync(userId   "");
        // 查询接受了多少条消息
        Long lastRows = messageservice.searchLastCount(userId);
        // 擦查询未读数据
        Long UnreadRows = messageservice.searchUnreadCount(userId);

        return R.ok().put("lastRows", lastRows).put("UnreadRows", UnreadRows);
    }

}

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgacahh
系列文章
更多 icon
同类精品
更多 icon
继续加载