文章目录
- 1、MongoDB综述
- 2、MongoDB基本操作
- 3、聚集操作
- [4、Spring Data MongoDB](#4、Spring Data MongoDB)
- 5、总结
1、MongoDB综述
MongoDB:是一个于分布式文件存储的数据库 ,高性能、无模式、文档性,目前nosql中最热门的数据库,开源产品,基于c++开发。
1_什么是MongoDB
介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的,它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。
MongoDB的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现了类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
特性
-
面向集合文档的存储:适合存储Bson(json的扩展)形式的数据;
-
格式自由,数据格式不固定,生产环境下修改结构都可以不影响程序运行;
-
强大的查询语句,面向对象的查询语言,基本覆盖sql语言所有能力;
-
完整的索引支持,支持查询计划;
-
支持复制和自动故障转移;
-
支持二进制数据及大型对象(文件)的高效存储;
-
使用分片集群提升系统扩展性;
-
使用内存映射存储引擎,把磁盘的IO操作转换成为内存的操作;
2_核心概念
为了更好的理解,下面与SQL中的概念进行对比:
SQL术语/概念 | MongoDB术语/概念 | 解释/说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 内嵌文档解决表连接 | 表连接,MongoDB不支持 |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
3_什么场景使用MongoDB
并没有某个业务场景必须要使用 MongoDB才能解决,但使用 MongoDB 通常能让你以更低的成本解决问题(包括学习、开发、运维等成本)。
应用特征 | Yes / No |
---|---|
应用不需要事务及复杂 join 支持 |
必须 Yes |
新应用,需求会变,数据模型无法确定,想快速迭代开发 | ? |
应用需要2000~3000以上的读写QPS(更高也可以) | ? |
应用需要TB甚至PB级别数据存储 | ? |
应用发展迅速,需要能快速水平扩展 | ? |
应用要求存储的数据不丢失 | ? |
应用需要99.999%高可用 | ? |
应用需要大量的地理位置查询、文本查询 | ? |
如果上述有1个 Yes,可以考虑 MongoDB,2个及以上的 Yes,选择MongoDB绝不会后悔!
MongoDB 的应用已经渗透到各个领域,比如游戏、物流、电商、内容管理、社交、物联网、视频直播等,以下是几个实际的应用案例:
-
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新。
-
物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
-
社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
-
物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
-
视频直播,使用 MongoDB 存储用户信息、礼物信息等。
不应该使用MongoDB的场景:
-
高度事务性系统:例如银行、财务等系统。MongoDB对事物的支持较弱;
-
传统的商业智能应用:特定问题的数据分析,多数据实体关联,涉及到复杂的、高度优化的查询方式;
-
使用sql方便的时候;数据结构相对固定,使用sql进行查询统计更加便利的时候;
4_部署安装
推荐使用Docker部署安装MongoDB:
sh
docker run -d \
--name mongodb \
-p 27017:27017 \
--restart=always \
-v mongodb:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=123456 \
mongo:4.4
#进入容器进行设置
docker exec -it mongodb /bin/bash
#进行认证
mongo -u "root" -p "123456" --authenticationDatabase "admin"
#测试命令,查看已有数据库
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
test 0.000GB
MongoDB默认会创建admin、config、local、test数据库。test库是一个默认的数据库,除了test库外admin、config、local库为系统库。admin
库主要存储MongoDB的用户、角色等信息,config
库主要存储分片集群基础信息,local
库主要存储副本集的元数据。
官网下载安装_Try MongoDB Atlas Products | MongoDB。
2、MongoDB基本操作
CRUD最基本的操作。
1_数据库 及 表(集合) 的操作
选择和创建数据库的语法格式:
sh
#说明:在MongoDB中,数据库是自动创建的,通过use切换到新数据库中,进行插入数据即可自动创建数据库
use 数据库名称
查看有权限查看的所有的数据库命令:
show dbs
或
show databases
admin
: 从权限的角度来看,这是"root"数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
local
: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
config
: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
集合的操作
新增:
db.createCollection(name)
隐式的方式
db.test_d.insert({u_id:1,goods_id:1});
集合的查询
show tables;
集合的命名规范:
- 集合名不能是空字符串 ""。
- 集合名不能含有 \0字符(空字符),这个字符表示集合名的结尾。
- 集合名不能以 "system."开头,这是为系统集合保留的前缀。
- 用户创建的集合名字不能含有保留字符。另外千万不要在名字里出现
$
。
示例:
sh
#查看所有的数据库
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
#通过use关键字切换数据库
> use admin
switched to db admin
#创建数据库
#说明:在MongoDB中,数据库是自动创建的,通过use切换到新数据库中,进行插入数据即可自动创建数据库
> use testdb
switched to db testdb
> show dbs #并没有创建数据库
admin 0.000GB
config 0.000GB
local 0.000GB
> db.user.insert({id:1,name:'zhangsan'}) #插入数据
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
testdb 0.000GB #数据库自动创建
#查看表
> show tables
user
> show collections
user
>
#删除集合(表)
> db.user.drop()
true #如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。
#删除数据库
> use testdb #先切换到要删除的数据库中
switched to db testdb
> db.dropDatabase() #删除数据库
{ "dropped" : "testdb", "ok" : 1 }
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
2_新增数据
在MongoDB中,存储的文档结构是一种类似于json的结构,称之为bson(全称为:Binary JSON)。
insert() 方法常用来新增数据,语法格式如下:
sh
db.collection.insert(
<document or arrary of documents> #要插入的文档记录
{
writeConcern:<document>,#写关注 默认 w:1 单实例部署下需要此实例的确认 复制模式下需要Primary实例确认。
ordered:<boolean>#是否按文档中的顺序进行插入。
}
)
参数说明:
- document or arrary:单条文档记录或数组包含的多条文档记录。
- writeConcern:写关注,默认w:1。
- ordered:当写入多条文档记录时,按数组中的顺序写入。
示例:
shell
#插入数据
#语法:db.COLLECTION_NAME.insert(document)
> db.user.insert({id:1,username:'zhangsan',age:20})
WriteResult({ "nInserted" : 1 })
# 可用来更新,insert插入已存在数据会报错,但是save不会,注意id并不是主键
> db.user.save({id:2,username:'lisi',age:25})
WriteResult({ "nInserted" : 1 })
> db.user.find() #查询数据
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }
-
_id 是集合中文档的主键,用于区分文档(记录),_id自动编入索引。
-
默认情况下,id 字段的类型为 ObjectID,是 MongoDB 的 BSON 类型之一,如果需要,用户还可以将 id 覆盖为 ObjectID 以外的其他内容。
-
ObjectID 长度为 12 字节,由几个 2-4 字节的链组成。每个链代表并指定文档身份的具体内容。以下的值构成了完整的 12 字节组合:
- 一个 4 字节的值,表示自 Unix 纪元以来的秒数
- 一个 3 字节的机器标识符
- 一个 2 字节的进程 ID
- 一个 3字节的计数器,以随机值开始
除了insert
外还可以使用insertOne
命令和insertMany
命令。insertOne
命令和insertMany
命令插入成功后可获取 _id 主键值(将插入成功的数据全部返回),而 insert 只返回记录数。
3_更新数据
update() 方法用于更新已存在的文档,所有修改操作在单条记录上都是"原子"的。语法格式如下:
shell
db.collection.update(
<query>,
<update>,
[
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>,
arraryFilters:[<filterdocument1>,.....],
hint:<document|string>
]
)
参数说明:
- query : update的查询条件,类似sql update查询内where后面的。
- update : update的对象和一些更新的操作符(如,inc...)等,也可以理解为sql update查询内set后面的
- upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
- multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
- writeConcern :可选,抛出异常的级别。
- arraryFilters:可选,如果被修改字段的值是一个数组,则可以指定修改数组中的哪个元素。
- hint:可选,当指定匹配文档时使用的索引,如果索引不存在则报错。
常用的修改操作符 | 作用 |
---|---|
$set | 修改集合字段 |
$unset | 删除集合字段 |
$rename | 字段重命名 |
$currentDate | 将字段的值修改为当前时间 |
$mul | 将字段的值乘以一个数字 |
$min | 比较要修改的字段值,如果原值比指定值大则修改为指定值,小则不修改 |
$max | 比较要修改的字段值,如果原值比指定值小则修改为指定值,大则不修改 |
上面的修改操作符对于嵌套文档同样适用,"."匹配要修改的字段即可。
shell
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }
> db.user.update({id:1},{$set:{age:22}}) #更新数据
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 22 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }
#注意:如果这样写,会删除掉其他的字段,所以需要加上$set
> db.user.update({id:1},{age:25})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }
#更新不存在的字段,会新增字段
> db.user.update({id:2},{$set:{sex:1}}) #更新数据
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }
#更新不存在的数据,默认不会新增数据
> db.user.update({id:3},{$set:{sex:1}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }
#如果设置第一个参数为true,就是新增数据
> db.user.update({id:3},{$set:{sex:1}},true)
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5c08cb281418d073246bc642")
})
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }
{ "_id" : ObjectId("5c08cb281418d073246bc642"), "id" : 3, "sex" : 1 }
# 删除指定字段
> db.user.update({id:2},{$unset:{age:"内容为任意值都无效"}})
# 字段重命名
> db.user.update({id:2},{$rename:{age:"age1"}})
# 设置字段值为当前时间 格式:{$currentDate:{字段名:{$type:"时间戳类型 timestamp/date"}
> db.user.update({id:2},{$currentDate:{today:{$type:"timestamp"}}})
# 将年龄字段的值变为原来的两倍
> db.user.update({id:2},{$mul:{age1:2}})
# 如果年龄大于40则修改为40
> db.user.update({id:2},{$min:{age1:40}})
同样的除了update外还可以使用,updateOne、updateMany和replaceOne进行更新。
updateOne、updateMany见名知意。replaceOne将集合中的文档记录替换成一条新的文档记录。
4_删除数据
通过remove()方法进行删除数据,语法如下:
shell
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)
参数说明:
- query :(可选)删除的文档的条件。
- justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
- writeConcern :(可选)抛出异常的级别。
shell
> db.user.remove({age:25})
WriteResult({ "nRemoved" : 2 }) #删除了2条数据
#插入4条测试数据
db.user.insert({id:1,username:'zhangsan',age:20})
db.user.insert({id:2,username:'lisi',age:21})
db.user.insert({id:3,username:'wangwu',age:22})
db.user.insert({id:4,username:'zhaoliu',age:22})
> db.user.remove({age:22},true)
WriteResult({ "nRemoved" : 1 }) #删除了1条数据
#删除所有数据
> db.user.remove({})
#说明:为了简化操作,官方推荐使用deleteOne()与deleteMany()进行删除数据操作。
db.user.deleteOne({id:1})
db.user.deleteMany({}) #删除所有数据
同样的,还有deleteOne、deleteMany这样的删除操作。
5_查询数据
MongoDB 查询数据的语法格式为:db.user.find([query],[fields])
- query :可选,使用查询操作符指定查询条件
- fields :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。
如果你需要以易读的方式来读取数据,可以使用 pretty()
方法,语法格式为:db.col.find().pretty()
。
排序 使用sort
方法,语法格式为:db.col.find().sort( { field : <1|-1> } )
。
分页查询 使用limit(m).skip(n)
,跳过 n 条数据查询 m 条数据。
条件查询:
操作 | 格式 | 范例 | RDBMS中的类似语句 |
---|---|---|---|
等于 | {:} | db.col.find({"by":"shenyang"}).pretty() | where by = 'shenyang' |
小于 | {:{$lt:}} | db.col.find({"likes":{$lt:50}}).pretty() | where likes < 50 |
小于或等于 | {:{$lte:}} | db.col.find({"likes":{$lte:50}}).pretty() | where likes <= 50 |
大于 | {:{$gt:}} | db.col.find({"likes":{$gt:50}}).pretty() | where likes > 50 |
大于或等于 | {:{$gte:}} | db.col.find({"likes":{$gte:50}}).pretty() | where likes >= 50 |
不等于 | {:{$ne:}} | db.col.find({"likes":{$ne:50}}).pretty() | where likes != 50 |
多值匹配 | {:{$in:}} | db.col.find({"likes":{$in:[50,100]}}).pretty() | where likes in (50,100) |
不在给定集合中 | {:{$nin:}} | db.col.find({"likes":{$nin:[50,100]}}).pretty() | where likes not in (50,100) |
逻辑操作符:
sql
{$operate:[{<expression1>},{<expression2>},.....]}
操作 | 格式 | 范例 | RDBMS中的类似语句 |
---|---|---|---|
或运算 | {$or:[{},{}]} | db.col.find({$or:[{"by":"shenyang"},{"likes":50}]}) | where by = 'shenyang' or likes=50 |
与运算 | {$and:[{},{}]} | db.col.find({$and:[{"by":"shenyang"},{"likes":50}]}) | where by = 'shenyang' and likes=50 |
非运算 | {$not:[{},{}]} | db.col.find({$not:{"by":"shenyang"}}) | where by != 'shenyang' |
字段名匹配,$exists
是表示否包含某个字段的操作符,语法格式如下:
sql
{ field: { $exists: <boolean> } }
其中 field 表示要匹配的字段。如果<boolean>
的值为 true,则返回包含此字段的文档记录;如果为false则返回不包含此字段的文档记录。
因为 MongoDB 的表结构是不固定的,有时需要返回包含或不包含某个字段的全部文档记录。$exists作用就显现出来了。
注意:MongoDB 中的$exists
与RDBMS中的exists不一样,关系型数据库中的exists与$in类似。
shell
#插入测试数据
db.user.insert({id:1,username:'zhangsan',age:20})
db.user.insert({id:2,username:'lisi',age:21})
db.user.insert({id:3,username:'wangwu',age:22})
db.user.insert({id:4,username:'zhaoliu',age:22})
db.user.find() #查询全部数据
db.user.find({},{id:1,username:1}) #只查询id与username字段,值为0则不显示
db.user.find().count() #查询数据条数
db.user.find({id:1}) #查询id为1的数据
db.user.find({age:{$lte:21}}) #查询小于等于21的数据
db.user.find({age:{$lte:21}, id:{$gte:2}}) #and查询,age小于等于21并且id大于等于2
db.user.find({$or:[{id:1},{id:2}]}) #查询id=1 or id=2
#分页查询:Skip()跳过几条,limit()查询条数
db.user.find().limit(2).skip(1) #跳过1条数据,查询2条数据
db.user.find().sort({id:-1}) #按照age倒序排序,-1为倒序,1为正序
另外,MongoDB还支持$regex将字段中的字符值进行正则匹配。
6_索引
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。
MongoDB支持的索引类型有:
-
单字段索引(Single Field)
- 支持所有数据类型中的单个字段索引
-
复合索引(Compound Index)
- 基于多个字段的索引,创建复合索引时要注意字段顺序与索引方向
-
多键索引(Multikey indexes)
- 针对属性包含数组数据的情况,MongoDB支持针对数组中每一个element创建索引。
-
全文索引(Text Index)
- 支持任意属性值为string或string数组元素的索引查询。
- 注意:一个集合仅支持最多一个Text Index,中文分词不理想,推荐Elasticsearch。
-
地理空间索引(Geospatial Index)
- 2dsphere索引,用于存储和查找球面上的点
- 2d索引,用于存储和查找平面上的点
-
哈希索引(Hashed Index)
- 针对属性的哈希值进行索引查询,当要使用Hashed index时,MongoDB能够自动的计算hash值,无需程序计算hash值。
- hash index仅支持等于查询,不支持范围查询。
我们重点需要掌握的是【单字段索引】、【2dsphere索引】。
shell
#单字段索引,1表示升序创建索引,-1表示降序创建索引
db.集合名.createIndex({"字段名":排序方式})
#2dsphere索引
db.集合名.createIndex({"字段名":"2dsphere"})
#示例,创建user集合,其中username和loc字段设置索引
db.user.createIndex({"username":1})
db.user.createIndex({"loc":"2dsphere"})
db.user.insert({id:1,username:'zhangsan',age:20,loc:{type:"Point",coordinates:[116.343847,40.060539]}})
db.user.insert({id:2,username:'lisi',age:22,loc:{type:"Point",coordinates:[121.612112,31.034633]}})
#查看索引
db.user.getIndexes()
#查看索引大小,单位:字节
db.user.totalIndexSize()
#删除索引
db.user.dropIndex("loc_2dsphere")
#或者,删除除了_id之外的索引
db.user.dropIndexes()
地理空间索引的type
可以是下列的类型:
- Point(坐标点),coordinates必须是单个位置
- MultiPoint(多个点),coordinates必须是位置数组
- LineString(线形),coordinates必须是两个或多个位置的数组
- MultiLineString(多行线形),coordinates必须是LineString坐标数组的数组
- Polygon(多边形),coordinates成员必须是 LinearRing 坐标数组的数组,必须是闭环,也就是第一个和最后一个坐标点要相同。
- MultiPolygon(多个多边形),coordinates成员必须是 Polygon 坐标数组的数组。
- GeometryCollection(几何集合),geometries是任何一个对象的集合。
7_地理信息查询
将地理数据存储为 GeoJSON 格式,使 MongoDB 支持地理空间的查询。GeoJSON格式的文件类似于一个嵌套的文档对象,语法格式如下:
sql
location: {
type: "Point",
coordinates:[23.356797, 30.578092]
}
其中,type 字段的值是指定地理位置坐标的数据格式。当type 字段的值为 Point时表示 coordinates 字段的数据代表地理空间中的一个点。
type字段还有其他类型的值,如LineString,表示coordinates 字段的数据代表一条线,语句如下:
sql
{
type: "LineString",
coordinates: [[30,6],[50,8]]
}
当type字段的值为 Polygon 时,表示 coordinates字段的数据代表一个闭环的多边形语句如下:
sql
{
type: "Polygon",
coordinates: [ [[0,0],[7,9],[8,2],[0,0]]]
}
type 还支持多个点 MultiPoint、多条线 MultiLineString、多个多边形 MultiPolygon。
下面以 type 为Point类型进行介绍,当 type 字段的值为 Point 时,coordinates 字段的值是一个数组,数组的第1个值为经度,范围为[-180,180],数组的第2个值为纬度范围为[-90,90],为了支持地理空间查询,还必须在 location 字段上创建一个地理位索引,语句如下:
sql
> db.address.createIndex({"location":"2dsphere"})
创建一个2dsphere类型的索引,接下来在这个索引上执行几种常用的地理空间查询。
地理查询操作符 | 作用 |
---|---|
$near | 指定一个地理空间的点,计算到此点的距离并由近及远地返回所有文档记录 |
$minDistance | 可选参数,表示返回距离此中心点最近距离为多少的点 |
$maxDistance | 可选参数,表示返回距离此中心点最远距离为多少的点 |
$geoWithin | 返回某一个区域内坐标点匹配的文档记录 |
$box | 当区域的类型为$box时,表示指定一个正方形 |
$centerSphere | 当区域的类型为$centerSphere 时,表示指定一个圆形 |
使用示例:
sql
# 指定一个地理空间的点,计算到此点的距离并由近及远地返回所有文档记录
> db.address.find({
"location":{
$near:{
$geometry:{
type: "Point","coordinates":[23.36,30.58]
$minDistance:10,
$maxDistance:100
}
}
}
})
# 输出结果
{" id" :10,"cust id":10,"location":{"type":"Point","coordinates":[23.36,30.58]}}
{"id":11,"cust id" :123,"location":{"type":"Point","coordinates":
[24.38,33.28]}}
{"id":12,"cust id":125,"location":{"type" :"Point","coordinates":[33.58,50.18]}}
# 返回某一个区域内坐标点匹配的文档记录
>db.address.find({
"location":{
$geoWithin:{
$geometry:{
type:"Polygon",
"coordinates":[
[
[0,0][10,60],[65,1],[0,0]
]]}}}})
#区域的类型由 Polygon指定为一个多边形,返回结果如下:
{" id" :13,"cust id":127,"location" :{"type" :"Point","coordinates":
[13.58,20.18]}}
{"_id" :10,"cust id":10,"location":{ "type" :"point","coordinates":
[23.36,30.58]}}
{"_id":11,"cust id":123,"location":{"type" :"point",
[24.38, 33.28]}}}
# 如果将上面多边形的值改为"coordinates":[[[0,0],[2,2],[3,1],[0,0]]],则输出结果为空说明指定的区域不包含任何一个位置的坐标。
# 当区域的类型为$box时,表示指定一个正方形,
> db.address.find( {
"location":{
$geoWithin:{
$box:[[0,0],[100,100]
]}}})
#当区域的类型为$centerSphere 时,表示指定一个圆形
>db.address.find({
"location":{
$geoWithin:{
$centerSphere:[[20,30],10
]}}})
#[20,30]表示圆心所在的经纬度,10表示半径,单位是miles。
8_批量写操作
MongoDB可以将所有的插入操作、修改操作、删除操作放在一个 bulkWrite 中,执行批量的写操作,bulkWrite的语法格式如下:
sql
db.collection.bulkwrite(
[<operation 1>, <operation 2>,......],
{
writeConcern : <document>,//可选参数,设置写关注
ordered : <boolean>
}
)
- [ <operation 1>, <operation 2>,.... ]是一个数组,其元素可以是 insentOne、updateOne、updateMany、deleteOne、deleteMany 和 replaceOne 中的任意一个多个。
ordered : <boolean>
:表示设置是否按数组中的操作命令顺序执行。当值为 true 时表示按顺序执行,当遇到错误的操作命令执行时,会中止后面的所有操作; 当为 fàlse 时,表示不按顺序执行,当到错误的操作命令执行时,不会中止后的所有操作,默认值为 true。
3、聚集操作
查询作都是在单个集合上执行的,并不能满足更加复杂的查询需求场景,如关系型数据库中的join操作、group分组操作,以及从多个集合中分别抽取所需字段构成一个新的集合并返回等操作。
如果想要实现这些功能,就需要使用MongoDB提供的高级查询框架,即聚集操作。
1_单个集合中的基础聚集函数
函数 | 作用 | 语法 |
---|---|---|
count() | 返回集合中的文档记录数量 | db.collection.count(query, options) |
estimatedDocumentCount() | 统计集合中的文档记录数量 | db.collection.estimatedDocumentCount(options) |
countDocuments() | 统计集合中的文档记录数量 | db.collection,countDocuments( < query >, < options >) |
distinct() | 获取指定字段的非重复值 | db.collection.distinct(field, query,options) |
- feld 参数:指定需要进行 distinct的字段名,为字符串类型
- query 参数:表示传入的查询过滤条件,为文档类型。
- options 参数:表示传人的可选项,为文档类型,控制被count的文档记录,如 limit(限制count的文档记录最大数)、skip(开始count前,跳过的文档记录数量),还有 hint、maxTimeMS 等选项。
estimatedDocumentCount() 与 count()函数的区别是,缺少查询过滤参数且在统计文档记录数量时使用的是集合中的元数据,并不是集合本身。
在正常情况下,estimatedDocumentCount()函数返回的统计结果与count()函数返回的统计结果相同。
如果 MongoDB 数据库实例是非正常关闭的,则从上一次 checkpoint 之后到发生的障这段时间内,可能有insent、delete、update 等操作发生,导致部分修改的数据并没有持久化,因此集合中的文档记录和集合对应的元数据(保存在sizeStorer.wt 文件中)于不一致的状态,这样使用 estimatedDocumentCount()函数统计的文档记录数量就会有所偏差,并不是正确的文档记录数量。
countDocuments()与count()函数不同的是,即使MongoDB数据库实例发生了意外故障,统计数据也不会采用集合中的元数据,而是直接扫描集合本身的数据。因此在这种异常场景下统计的文档记录数量也是正确的。
sql
#通过limit选项限制返回的文档记录数
>db.customers.count({},{limit:1})
#注意:即使不传入任何查询条件,第1个参数也是必选项,需要传递。
>db.customers.countDocuments({})
2_管道聚集框架
为了在多个集合上执行查询分析,MongoDB提供了流水线式的管道聚集框架。这里所说的管道类似于 UNIX上的管道命令,数据通过一个多步骤的管道依次进行处理最后返回一个聚集结果。管道提供了高效的数据分析流程,是MongoDB首选的大数分析方法,一个典型的管道聚集操作流程如图所示。
上面的管道聚集操作流程对应的执行语句如下:
sql
>db.books.aggregate
([
{$match:{ status: "normal"}},
{$group:{ _id:"$book_id",total:{ $sum: "$num"}}}
])
第一步:通过管道操作符 $match 过滤所有 status 属性值等于normal的文档记录,然后将这些文档记录传递给管道中的下一个步骤进行处理。
第二步:通过管道操作符 $group进行分组,分组按照book_id进行,同时对具有相同 book_id的文档记录按num字段求和,返回值中包含 id字段和 total字段。
上面的操作相当于如下SQL语句:
sql
Select book_id,sum(num)as total from books where status ="normal" group by book_id;
总体来说,管道聚集的语法格式如下:
sql
db.collection,aggregate(pipeline,options)
-
pipeline 参数: 为数组类型,包含一系列由管道操作符构成的数据处理步骤,语法格式为
db.collection.aggregate([{<stage>},...])
,其中,有些管道操作符可以出现多次,常用的管道操作符及其说明如下。管道操作符 作用 $project 控制哪些字段返回,如 id:0,name:1 返回name字段值,过滤 id字段值。 $match 过滤文档记录,只将匹配的文档记录传递到管道中的下一个步骤。 $limit 限制管道中文档记录的数量。 $skip 跳过指定数量的文档记录,返回剩下的文档记录。 $sort 对所有输入的文档记录进行排序。 $group 对所有文档记录进行分组然后计算聚集结果。 $lookup 实现集合之间的join 操作。 $addFields 向集合中添加新字段。 $out 将管道中的文档记录输出到一个具体的集合中,这个必须是管道操作中的最后一步。 与$group操作符一起使用的计算聚集值的操作符及其说明如下。
累计操作符 作用 $first 返回$group操作后的第1值。 $last 返回$group操作后的最后一个值。 $max 返回$group操作后的最大值。 $min 返回$group操作后的最小值 $avg 返回$group 操作后的平均值。 $sum 返回$group操作后所有值的和。 -
options参数: 表示可选参数,为文档类型,包含allowDiskUse(当allowDiskUse的值为true时,在聚集过程中可以将数据写到dbPath路径下的临时目录)、maxTimeMS(设置该操作的最大超时时间)、readConcern(设置读关注)、writeConcern(设置写关注)等常用字段。
下面通过实例对常用的管道操作符进行详细说明。
sql
#按 book_id 进行分类,统计每种类型的图书(status为sold)的销售总数量,同时按数量进行降序排列
>db.books.aggregate([{$match :{"status":"sold" }},{$group:{_id:"$book_id",total: {$sum:"$num"}}},{$sort:{total:-1}}]}
集合文档记录传入管道后,依次通过 m a t c h ( 过滤 ) 、 g r o u p ( 分组 ) 、 s o r t ( 排序 ) match(过滤)、group(分组)、sort(排序) match(过滤)、group(分组)、sort(排序) 3个步骤进行处理。
这里$group管道操作符的标准语法格式如下:
sql
$group:
{
_id:<expression>,
<field1>:{<accumulator1>:<expression1>,
...
}
- _id 作为结果集中的必填字段名,设置需要进行分组的字段或字段的组合。
- < field1>作为结果集中的返回字段名,取值通过< accumulator1>累计操作符(如$ sum、$ avg、$max等)和表达式< expression1>(给< accumulator1>累计操作符传递参数变量)计算得到。
当需要返回新的字段名时,可以通过$addFields
添加新字段来实现,语句如下:
sql
>db.books.aggregate([
{$match :{"status":"sold" }},
{$group:{_id:"$book_id",total: {$sum:"$num"}}},
{$addFields:{book_id:"$_id"}},
{$sort:{total:-1}}
]}
#输出结果如下:
{"_id":4,"total":300,"book_id":4}
{"_id":3,"total":300,"book_id":3}
{"_id":1,"total":200,"book_id":1}
{"_id":2,"total":100,"book_id":2}
#相当于如下SQL
Select book_id as _id,sum(num)as total,book_id from books where status ="sold"
group by book_id order by total desc;
其中,管道步骤{$addFields:{book_id:"$_id"}}
,中的 book_id 为新字段的名称;$_id变量为上一步骤,即 $group管道步骤输出的_id字段。
l o o k u p 关联查询 lookup 关联查询 lookup关联查询
从MongoDB 3.2版本开始,引人$lookup关联查询操作,解决类似于关系型数据库下的 join 查询需求,其语法格式如下
sql
db.books.aggregate([{
$lookup:
{
from:<被join 的目标表>,
localField:<源表中的字段>,
foreignField:<被join 的目标表中的字段>,
as:<返回满足 join 条件下,被 join 的目标表中的文档记录>
}
}])
示例:
sql
#集合数据如下
>db.booksAttr.find({})
{"_id":1,"type_id":1,"type_name" :"science"}
{"_id":2,"type_id":2,"type_name" :"human"}
{"_id":3,"type_id":3,"type_name" :"music"}
{"_id":4,"type_id":4,"type_name" :"sports"}
# 其中,type_id字段与 books 集合的 book_id关联,
# 但由于经过 $group后 后面的文档记录发生改变,只包含_id和total两个字段。
# 因此,localField字段值为_id,而不是原来books集合中的book_id字段。
>db.books.aggregate([
{$match :{"status":"sold" }},
{$group:{_id:"$book_id",total: {$sum:"$num"}}},
{$lookup:{
from:"booksAttr",
localField:"_id",
foreignField:"type_id",
as:"booksAttr"
}},
{$sort:{total:-1}}
]}
#结果如下
{"_id":4,"total":300,"booksAttr":[{"_id" :4,"type_id":4,"type_name":"soprts"}]}
{"_id":3,"total":300,"booksAttr":[{"_id" :3,"type_id":3,"type_name":"music"}]}
{"_id":1,"total":200,"booksAttr":[{"_id" :1,"type_id":1,"type_name":"science"}]}
{"_id":2,"total":100,"booksAttr":[{"_id" :2,"type_id":2,"type_name":"human"}]}
$lookup 管道操作符的执行流程如图:
可以看到,通过上述4个步骤后,默认将文档记录的所有字段都关联了,而且关联的所有文档记录构成一个数组,并将数组的值赋给as
选项指定的字段名booksAttr。
3_MapReduce
MongoDB也提供了当前流行的MapReduce并行编程模型,为海量数据的查询分析提供了一种有效方法,利用MongoDB进行分布式存储,再利用MapReduce进行分析(db.collection.mapReduce(function(){},function(){})
)。
略
4、Spring Data MongoDB
spring-data对MongoDB做了支持,使用spring-data-mongodb可以简化MongoDB的操作。
https://spring.io/projects/spring-data-mongodb。
1_创建工程
创建测试工程对Spring Data MongoDB的使用做基本的学习。导入依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
2_配置
yaml
spring:
application:
name: test
data:
mongodb:
host: 192.168.193.141
port: 27017
database: test
authentication-database: admin #认证数据库
username: root
password: "123456"
auto-index-creation: true #自动创建索引
3_Entity
java
package com.test.mongo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document("test_person") //指定表名
public class Person {
@Id // 标识为主键
private ObjectId id;
@Indexed //标识索引字段
private String name;
private int age;
/**
* 用户位置
* x: 经度,y:纬度
*/
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private GeoJsonPoint location;
//存储嵌套对象数据
private Address address;
}
java
package com.test.mongo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document("test_address") //指定表名
public class Address {
private String street;
private String city;
private String zip;
}
4_Service
java
package com.test.mongo.service;
import com.test.mongo.entity.Person;
import java.util.List;
public interface PersonService {
/**
* 新增数据
*
* @param person 数据
*/
void savePerson(Person person);
/**
* 更新数据
*
* @param person 数据
*/
void update(Person person);
/**
* 根据名字查询用户列表
*
* @param name 用户名字
* @return 用户列表
*/
List<Person> queryPersonListByName(String name);
/**
* 分页查询用户列表
*
* @param page 页数
* @param pageSize 页面大小
* @return 用户列表
*/
List<Person> queryPersonPageList(int page, int pageSize);
/**
* 根据id删除用户
*
* @param id 主键
*/
void deleteById(String id);
}
接口实现类:
java
package com.test.mongo.service.impl;
import com.test.mongo.entity.Person;
import com.test.mongo.service.PersonService;
import org.bson.types.ObjectId;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class PersonServiceImpl implements PersonService {
@Resource
private MongoTemplate mongoTemplate;
@Override
public void savePerson(Person person) {
this.mongoTemplate.save(person);
}
@Override
public void update(Person person) {
//条件
Query query = Query.query(Criteria.where("id").is(person.getId()));
//更新的数据
Update update = Update.update("age", person.getAge())
.set("name", person.getName())
.set("location", person.getLocation())
.set("address", person.getAddress());
//更新数据
this.mongoTemplate.updateFirst(query, update, Person.class);
}
@Override
public List<Person> queryPersonListByName(String name) {
Query query = Query.query(Criteria.where("name").is(name)); //构造查询条件
return this.mongoTemplate.find(query, Person.class);
}
@Override
public List<Person> queryPersonPageList(int page, int pageSize) {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
Query query = new Query().with(pageRequest);
return this.mongoTemplate.find(query, Person.class);
}
@Override
public void deleteById(String id) {
Query query = Query.query(Criteria.where("id").is(new ObjectId(id)));
this.mongoTemplate.remove(query, Person.class);
}
}
5_启动类
java
package com.test.mongo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MongoApplication {
public static void main(String[] args) {
SpringApplication.run(MongoApplication.class, args);
}
}
6_测试
java
package com.test.mongo.service;
import com.test.mongo.entity.Address;
import com.test.mongo.entity.Person;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import javax.annotation.Resource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class PersonServiceTest {
@Resource
PersonService personService;
@Test
void savePerson() {
Person person = Person.builder()
.id(ObjectId.get())
.name("张三")
.age(20)
.location(new GeoJsonPoint(116.343847, 40.060539))
.address(new Address("人民路", "上海市", "666666")).build();
this.personService.savePerson(person);
}
@Test
void update() {
Person person = Person.builder()
.id(new ObjectId("632283c08139e535c2bd7579"))
.name("张三")
.age(22) //修改数据
.location(new GeoJsonPoint(116.343847, 40.060539))
.address(new Address("人民路", "上海市", "666666")).build();
this.personService.update(person);
}
@Test
void queryPersonListByName() {
List<Person> personList = this.personService.queryPersonListByName("张三");
personList.forEach(System.out::println);
}
@Test
void queryPersonPageList() {
List<Person> personList = this.personService.queryPersonPageList(1, 10);
personList.forEach(System.out::println);
}
@Test
void deleteById() {
this.personService.deleteById("632283c08139e535c2bd7579");
}
}
5、总结
MongoDB与关系型数据库的对比:
MongoDB | 关系型数据库 | |
---|---|---|
亿级以上数据量 | 轻松支持 | 分库分表 |
灵活表结构 | 轻松支持 | 关联查询比较痛苦 |
高并发读 | 轻松支持 | 需要优化 |
高并发写 | 轻松支持 | 需要优化 |
跨地区集群 | 轻松支持 | 需要定制方案 |
分片集群 | 轻松支持 | 需要中间件 |
地理位置查询 | 完整的地理位置 | PG可以,其他数据库麻烦 |
聚合计算 | 功能很强大 | 使用Group By性能能有限 |
异构数据 | 轻松支持 | 使用 EKV 属性表 |
大宽表 | 轻松支持 | 性能受限 |