1、MongoDB数据模型
问题: M o n g o D B 为什么会使用 B S O N ? \color{red}{问题:MongoDB为什么会使用BSON?} 问题:MongoDB为什么会使用BSON?
1.1 BSON协议与数据类型
1.1.1 JSON
JSON是当今非常通用的一种跨语言Web数据交互格式,属于ECMAScript标准规范的一个子集。 J S O N ( J a v a S c r i p t O b j e c t N o t a t i o n , J S 对象简谱)即 J a v a S c r i p t 对象表示法, \color{red}{JSON(JavaScript Object Notation, JS对象简谱)即JavaScript对象表示法,} JSON(JavaScriptObjectNotation,JS对象简谱)即JavaScript对象表示法,它是JavaScript对象的一种文本表现形式。
作为一种轻量级的数据交换格式, J S O N 的可读性非常好,而且非常便于系统生成和解析 \color{red}{JSON的可读性非常好,而且非常便于系统生成和解析} JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式
JSON定义了6种数据类型:
Type | 解释 |
---|---|
string | 字符串 |
number | 数值 |
object | js的对象形式,用{key:value}表示,可嵌套 |
array | 数组,可嵌套 |
true|false | 布尔类型 |
null | 空值 |
大多数情况下,使用JSON作为数据交互格式已经是理想的选择,
J
S
O
N
基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编
/
解码格式
\color{red}{JSON基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编/解码格式}
JSON基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编/解码格式
- 在微服务架构中,使用gRPC(基于Google的Protobuf)可以获得更好的网络利用率
- 分布式中间件、数据库,使用私有定制的TCP数据包格式来提供高性能、低延时的计算能力
1.1.2 BSON
- BSON由10gen团队设计并开源,目前主要用于MongoDB数据库
- BSON(Binary JSON)是二进制版本的JSON,其在性能方面有更优的表现
- BSON在许多方面和JSON保持一致,其同样也支持内嵌的文档对象和数组结构
对比:
J
S
O
N
是基于文本的,而
B
S
O
N
则是二进制(字节流)编
/
解码的形式
\color{red}{JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式}
JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式
在空间的使用上,
B
S
O
N
相比
J
S
O
N
并没有明显的优势
\color{red}{在空间的使用上,BSON相比JSON并没有明显的优势}
在空间的使用上,BSON相比JSON并没有明显的优势
MongoDB在文档存储、命令协议上都采用了BSON作为编/解码格式,主要具有如下优势:
- 类JSON的轻量级语义,支持简单清晰的嵌套、数组层次结构, 可以实现模式灵活的文档结构 \color{red}{可以实现模式灵活的文档结构} 可以实现模式灵活的文档结构
- 更高效的遍历 \color{red}{更高效的遍历} 更高效的遍历,BSON在编码时会记录每个元素的长度,可以直接通过seek操作进行元素的内容读取,相对JSON解析来说,遍历速度更快
- 更丰富的数据类型 \color{red}{更丰富的数据类型} 更丰富的数据类型,除了JSON的基本数据类型,BSON还提供了MongoDB所需的一些扩展类型,比如日期、二进制数据等,这更加方便数据的表示和操作
- M o n g o D B 中,一个 B S O N 文档最大大小为 16 M ,文档嵌套的级别不超过 100 \color{red}{MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100} MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100
BSON的数据类型
BsonType
$type操作符:基于BSON类型来检索集合中匹配的数据类型
db.test.find({"title" : {$type : 2}})
## 或者
db.test.find({"title" : {$type : "string"}})
2、插入文档
MongoDB 使用 insert() 或 save() 方法向集合中插入文档,语法如下:
db.COLLECTION_NAME.insert(document)
#或
db.COLLECTION_NAME.save(document)
- save():如果 _id 主键存在则更新数据,如果不存在就插入数据。 该方法新版本中已废弃 \color{red}{该方法新版本中已废弃} 该方法新版本中已废弃,可以使用 db.collection.insertOne() 或 db.collection.replaceOne() 来代替。
- insert(): 若插入的数据主键已经存在, 则会抛 o r g . s p r i n g f r a m e w o r k . d a o . D u p l i c a t e K e y E x c e p t i o n 异常 \color{red}{则会抛 org.springframework.dao.DuplicateKeyException 异常} 则会抛org.springframework.dao.DuplicateKeyException异常, 提示主键重复,不保存当前数据 \color{red}{提示主键重复,不保存当前数据} 提示主键重复,不保存当前数据
3.2 版本之后新增了 db.collection.insertOne() 和 db.collection.insertMany()
2.1 新增单个文档
db.collection.insertOne() 用于向集合插入一个新文档,语法格式如下:
db.collection.insertOne(
<document>,
{
writeConcern: <document>
}
)
w r i t e C o n c e r n 决定一个写操作落到多少个节点上才算成功。 w r i t e C o n c e r n 的取值包括: \color{red}{writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括: } writeConcern决定一个写操作落到多少个节点上才算成功。writeConcern的取值包括:
- 0:发起写操作,不关心是否成功;
- 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
- majority:写操作需要被复制到大多数节点上才算成功。
testdb> use testdb
already on db testdb
testdb> db.testCollection.save({x:1})
TypeError: db.testCollection.save is not a function
testdb> db.testCollection.insertOne({x:1})
{
acknowledged: true,
insertedId: ObjectId("63f884d67b9fcb1f8bf84df1")
}
2.2 批量新增文档
db.collection.insertMany(
[ <document 1> , <document 2>, ... ],
{
writeConcern: <document>,
ordered: <boolean>
}
)
参数说明: \color{red}{参数说明:} 参数说明:
- document:要写入的文档。
- writeConcern:写入策略,默认为 1,即要求确认写操作,0 是不要求。
- ordered:指定是否按顺序写入,默认 true,按顺序写入。
测试:批量插入100条随机数据
编辑脚本testCollection.js
var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<100;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: "xxw"+i
};
books.push(book)
}
db.testCollection.insertMany(books);
进入mongo shell,执行
load("testCollection.js")
3、查询文档
MongoDB 查询数据的语法格式如下:
db.collection.find(query, projection)
//findOne查询集合中的第一个文档
db.collection.findOne(query, projection)
- query :可选,使用查询操作符指定查询条件
- projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。
投影时,
_
i
d
为
1
的时候,其他字段必须是
1
\color{red}{投影时,\_id为1的时候,其他字段必须是1}
投影时,_id为1的时候,其他字段必须是1
_
i
d
是
0
的时候,其他字段可以是
0
\color{red}{ \_id是0的时候,其他字段可以是0}
_id是0的时候,其他字段可以是0
如果没有
_
i
d
字段约束,多个其他字段必须同为
0
或同为
1
\color{red}{ 如果没有\_id字段约束,多个其他字段必须同为0或同为1}
如果没有_id字段约束,多个其他字段必须同为0或同为1
如果查询返回的条目数量较多,mongo shell则会自动实现分批显示。默认情况下每次只显示20条,可以输入it命令读取下一批
如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:
db.collection.find().pretty()
pretty() 方法以格式化的方式来显示所有文档
3.1 条件查询
3.1.1 MongoDB 与 SQL 语法区分
MongoDB 与 RDBMS Where 语句比较
操作 | 格式 | 范例 | RDBMS中类似语句 |
---|---|---|---|
等于 | {<key>:<value>} | db.testCollection.find({“title”:“book-18”}).pretty() | where title = “book-18” |
小于 | {<key>:{$lt:<value>}} | db.testCollection.find({“favCount”:{$lt:2}}).pretty() | where favCount < 2 |
小于等于 | {<key>:{$lte:<value>}} | db.testCollection.find({“favCount”:{$lte:2}}).pretty() | where favCount <= 2 |
大于 | {<key>:{$gt:<value>}} | db.testCollection.find({“favCount”:{$gt:2}}).pretty() | where favCount > 2 |
大于等于 | {<key>:{$gte:<value>}} | db.testCollection.find({“favCount”:{$gte:6}}).pretty() | where favCount >= 6 |
不等于 | {<key>:{$ne:<value>}} | db.testCollection.find({“favCount”:{$ne:6}}).pretty() | where favCount != 6 |
查询逻辑对照表
SQL | MQL |
---|---|
a=1 and b=1 | {a:1,b:1} 或者 {$and:[{a:1},{b:1}]} |
a=2 or b=3 | {$or:[{a:2},{b:3}]} |
a is null | {a:{$exists:false}} |
a in (3,2,1) | {a:$in:[1,2,3]} |
查询逻辑运算符
- $lt: 存在并小于
- $lte: 存在并小于等于
- $gt: 存在并大于
- $gte: 存在并大于等于
- $ne: 不存在或存在但不等于
- $in: 存在并在指定数组中
- $nin: 不存在或不在指定数组中
- $or: 匹配两个或多个条件中的一个
- $and: 匹配全部条件
3.1.2 MongoDB 查询实例
MongoDB AND 条件
db.col.find({key1:value1, key2:value2}).pretty()
MongoDB OR 条件
db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
AND 和 OR 联合使用
以下实例演示了 AND 和 OR 联合使用,类似常规 SQL 语句为: ’ title = “book-18” AND (favCount <10 OR favCount != 6)’
db.testCollection.find({
"title":"book-18",
$or:[
{"favCount":{$lte:10}},
{"favCount":{$ne:6}}
]
}).pretty()
testdb> db.testCollection.find({
... "title":"book-18",
... $or:[
... {"favCount":{$lte:10}},
... {"favCount":{$ne:6}}
... ]
... }).pretty()
[
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84e04"),
title: 'book-18',
type: 'technology',
tag: 'document',
favCount: 52,
author: 'xxw18'
}
]
3.1.2 MongoDB 排序与分页(Limit、sort与Skip方法)
- 如果你需要在MongoDB中读取指定数量的数据记录,可以使用MongoDB的Limit方法,limit()方法接受一个数字参数,该参数指定从MongoDB中读取的记录条数
注:如果没有指定 l i m i t ( ) 方法中的参数则显示集合中的所有数据 \color{red}{注:如果没有指定limit()方法中的参数则显示集合中的所有数据} 注:如果没有指定limit()方法中的参数则显示集合中的所有数据
db.COLLECTION_NAME.find().limit(NUMBER)
- 我们除了可以使用limit()方法来读取指定数量的数据外,还可以使用skip()方法来跳过指定数量的数据,skip方法同样接受一个数字参数作为跳过的记录条数
注 : s k i p ( ) 方法默认参数为 0 \color{red}{ 注:skip()方法默认参数为 0} 注:skip()方法默认参数为0
db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
testdb> db.testCollection.find({"favCount":{$lte:200}}).limit(1).skip(1).pretty()
[
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84df3"),
title: 'book-1',
type: 'novel',
tag: 'mongodb',
favCount: 20,
author: 'xxw1'
}
]
- 在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列
db.COLLECTION_NAME.find().sort({KEY:1})
testdb> db.testCollection.find({"favCount":{$lte:200}}).limit(3).skip(1).sort({"favCount":1}).pretty()
[
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84e0f"),
title: 'book-29',
type: 'sociality',
tag: 'developer',
favCount: 6,
author: 'xxw29'
},
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84df7"),
title: 'book-5',
type: 'sociality',
tag: 'mongodb',
favCount: 11,
author: 'xxw5'
},
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84e08"),
title: 'book-22',
type: 'novel',
tag: 'nosql',
favCount: 12,
author: 'xxw22'
}
]
3.1.3 分页问题的处理
数据量大的时候,应该避免使用
s
k
i
p
/
l
i
m
i
t
形式的分页
\color{red}{ 数据量大的时候,应该避免使用skip/limit形式的分页}
数据量大的时候,应该避免使用skip/limit形式的分页
替代方案:使用查询条件
+
唯一排序条件
\color{red}{ 替代方案:使用查询条件+唯一排序条件}
替代方案:使用查询条件+唯一排序条件
第一页:db.testCollection.find({}).sort({_id: 1}).limit(20);
第二页:db.testCollection.find({_id: {KaTeX parse error: Expected 'EOF', got '}' at position 17: …t: <第一页最后一个_id>}̲}).sort({_id: 1…gt: <第二页最后一个_id>}}).sort({_id: 1}).limit(20);
处理分页问题–避免使用
c
o
u
n
t
\color{red}{ 处理分页问题 – 避免使用 count }
处理分页问题–避免使用count
尽可能不要计算总页数,特别是数据量大和查询条件不能完整命中索引时。
考虑以下场景:假设集合总共有 1000w 条数据,在没有索引的情况下考虑以下查询:
//只需要遍历前 n 条,直到找到 50 条 x=100 的文档即可结束
db.testCollection.find({x: 100}).limit(50);
// 需要遍历完 1000w 条找到所有符合要求的文档才能得到结果。 为了计算总页数而进行的 count() 往往是拖慢页面整体加载速度的原因
db.testCollection.count({x: 100});
3.1.4 正则表达式匹配查询
MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式
//使用正则表达式查找type包含 so 字符串的book
db.col.find({type:{$regex:"so"}})
//或者
db.col.find({type:/so/})
4、更新文档
可以用update命令对指定的数据进行更新,命令的格式如下:
db.collection.update(query,update,options)
- query:描述更新的查询条件;
- update:描述更新的动作及新的内容;
- options:描述更新的选项
- upsert: 可选,如果不存在update的记录,是否插入新的记录。默认false,不插入
- multi: 可选,是否按条件查询出的多条记录全部更新。 默认false,只更新找到的第一条记录
- writeConcern :可选,决定一个写操作落到多少个节点上才算成功。
4.1 更新操作符
操作符 | 格式 | 描述 |
---|---|---|
$set | {$set:{filed:value}} | 指定一个键并更新值,若键不存在则创建 |
$unset | {$unset:{filed:value}} | 删除一个键 |
$inc | {$inc:{f:v}} | 对数值类型进行增减 |
$rname | {$rname:{old_f_name:new_f_name}} | 修改字段名 |
$push | {$push:{f:v}} | 将数值追加到数组中,若数组不存在则会进行初始化 |
$pushAll | {$pushAll:{f:v}} | 追加多个值到一个数组字段内 |
$pull | {$pull:{f:_v}} | 从数组中删除指定元素 |
$addToSet | {$addToSet:{f:v}} | 添加元素到数组中,具有排重功能 |
$pop | {$pop:{filed:1}} | 删除数组的第一个或最后一个元素 |
4.1.1 更新单个文档
文档的favCount字段自增
testdb> db.testCollection.update({_id:ObjectId("63f88c0b7b9fcb1f8bf84e08")},{$inc:{favCount:1}})
DeprecationWarning: Collection.update() is deprecated. Use updateOne, updateMany, or bulkWrite.
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
DeprecationWarning: Collection.update() is deprecated. Use updateOne, updateMany, or bulkWrite.
u
p
d
a
t
e
方法已被过时,请使用:
u
p
d
a
t
e
O
n
e
,
u
p
d
a
t
e
M
a
n
y
或
b
u
l
k
W
r
i
t
e
\color{red}{ update方法已被过时,请使用:updateOne,updateMany 或 bulkWrite}
update方法已被过时,请使用:updateOne,updateMany或bulkWrite
testdb> db.testCollection.updateOne({_id:ObjectId("63f88c0b7b9fcb1f8bf84e08")},{$inc:{favCount:1}})
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
4.1.2 更新多个文档
默认情况下,update命令只在更新第一个文档之后返回,如果需要更新多个文档,则可以使用multi选项。
将分类为“novel”的文档的增加发布时间(publishedDate)
db.testCollection.update({type:"novel"},{$set:{publishedDate:new Date()}},{"multi":true})
m u l t i : 可选, m o n g o d b 默认是 f a l s e , 只更新找到的第一条记录,如果这个参数为 t r u e , 就把按条件查出来多条记录全部更新 \color{red}{ multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新} multi:可选,mongodb默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新
update命令的选项配置较多,为了简化使用还可以使用一些快捷命令:
- updateOne:更新单个文档。
- updateMany:更新多个文档。
- replaceOne:替换单个文档。
4.1.3 使用upsert命令
upsert是一种特殊的更新,其表现为如果目标文档不存在,则执行插入命令
testdb> db.testCollection.update(
... {title:"my book"},
... {$set:{tags:["nosql","mongodb"],type:"none",author:"xxx"}},
... {upsert:true}
... )
{
acknowledged: true,
insertedId: ObjectId("63f8e4bb605acd95f5b3ffcd"),
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
}
m a t c h e d C o u n t 、 m o d i f i e d C o u n t 都为 0 ,表示没有文档被匹配及更新, u p s e r t e d C o u n t = 1 提示执行了 u p s e r t 动作 \color{red}{matchedCount、modifiedCount都为0,表示没有文档被匹配及更新,upsertedCount=1提示执行了upsert动作} matchedCount、modifiedCount都为0,表示没有文档被匹配及更新,upsertedCount=1提示执行了upsert动作
4.1.4 findAndModify命令
findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档
//将某个book文档的收藏数(favCount)加1
db.testCollection.findAndModify({
query:{_id:ObjectId("63f8e4bb605acd95f5b3ffcd")},
update:{$inc:{favCount:1}}
})
//该操作会返回符合查询条件的文档数据,并完成对文档的修改。
{
_id: ObjectId("63f8e4bb605acd95f5b3ffcd"),
title: 'my book',
author: 'xxx',
tags: [ 'nosql', 'mongodb' ],
type: 'none'
}
默认情况下,
f
i
n
d
A
n
d
M
o
d
i
f
y
会返回修改前的“旧”数据。
\color{red}{默认情况下,findAndModify会返回修改前的“旧”数据。}
默认情况下,findAndModify会返回修改前的“旧”数据。
如果希望返回修改后的数据,则可以指定new选项
db.testCollection.findAndModify({
query:{_id:ObjectId("63f8e4bb605acd95f5b3ffcd")},
update:{$inc:{favCount:1}},
new:true
})
{
_id: ObjectId("63f8e4bb605acd95f5b3ffcd"),
title: 'my book',
author: 'xxx',
tags: [ 'nosql', 'mongodb' ],
type: 'none',
favCount: 2
}
与findAndModify语义相近的命令如下:
- findOneAndUpdate:更新单个文档并返回更新前(或更新后)的文档。
- findOneAndReplace:替换单个文档并返回替换前(或替换后)的文档。
5、删除文档
5.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)
testdb> db.testCollection.remove({type:"novel"},true)
DeprecationWarning: Collection.remove() is deprecated. Use deleteOne, deleteMany, findOneAndDelete, or bulkWrite.
{ acknowledged: true, deletedCount: 1 }
5.2 使用 delete 删除文档
官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档,语法格式如下:
db.testCollection.deleteMany ({}) //删除集合下全部文档
db.testCollection.deleteMany ({ type:"novel" }) //删除 type等于 novel 的全部文档
db.testCollection.deleteOne ({ type:"novel" }) //删除 type等于novel 的一个文档
注意:
r
e
m
o
v
e
、
d
e
l
e
t
e
M
a
n
y
等命令需要对查询范围内的文档逐个删除
\color{red}{注意: remove、deleteMany等命令需要对查询范围内的文档逐个删除}
注意:remove、deleteMany等命令需要对查询范围内的文档逐个删除
,如果希望删除整个集合,则使用
d
r
o
p
命令会更加高效
\color{red}{,如果希望删除整个集合,则使用drop命令会更加高效}
,如果希望删除整个集合,则使用drop命令会更加高效
5.2.1 返回被删除文档
remove、deleteOne等命令在删除文档后只会返回确认性的信息,如果希望获得被删除的文档,则可以使用findOneAndDelete命令
db.testCollection.findOneAndDelete({type:"novel"})
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84df3"),
title: 'book-1',
type: 'novel',
tag: 'mongodb',
favCount: 20,
author: 'xxw1'
}
除了在结果中返回删除文档,findOneAndDelete命令还允许定义“删除的顺序”,即按照指定顺序删除找到的第一个文档
db.testCollection.findOneAndDelete({type:"novel"},{sort:{favCount:1}})
{
_id: ObjectId("63f88c0b7b9fcb1f8bf84e08"),
title: 'book-22',
type: 'novel',
tag: 'nosql',
favCount: 14,
author: 'xxw22'
}
remove、deleteOne等命令只能按默认顺序删除,利用这个特性,
f
i
n
d
O
n
e
A
n
d
D
e
l
e
t
e
可以实现队列的先进先出
\color{red}{findOneAndDelete可以实现队列的先进先出}
findOneAndDelete可以实现队列的先进先出
6、总结
文档操作最佳实践
关于文档结构 \color{red}{关于文档结构 } 关于文档结构
-
防止使用太长的字段名(浪费空间)
-
防止使用太深的数组嵌套(超过2层操作比较复杂)
-
不使用中文,标点符号等非拉丁字母作为字段名
关于写操作 \color{red}{关于写操作} 关于写操作 -
update 语句里只包括需要更新的字段
-
尽可能使用批量插入来提升写入性能
-
使用TTL自动过期日志类型的数据