二、MongoDB数据模型和文档操作

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对象表示法,} JSONJavaScriptObjectNotation,JS对象简谱)即JavaScript对象表示法,它是JavaScript对象的一种文本表现形式。

作为一种轻量级的数据交换格式 J S O N 的可读性非常好,而且非常便于系统生成和解析 \color{red}{JSON的可读性非常好,而且非常便于系统生成和解析} JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式

JSON定义了6种数据类型:

Type解释
string字符串
number数值
objectjs的对象形式,用{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
MongoDB-BSON-DATATYPE
$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} 投影时,_id1的时候,其他字段必须是1
_ i d 是 0 的时候,其他字段可以是 0 \color{red}{ \_id是0的时候,其他字段可以是0} _id0的时候,其他字段可以是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

查询逻辑对照表

SQLMQL
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方法已被过时,请使用:updateOneupdateManybulkWrite

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动作} matchedCountmodifiedCount都为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等命令需要对查询范围内的文档逐个删除} 注意:removedeleteMany等命令需要对查询范围内的文档逐个删除
,如果希望删除整个集合,则使用 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自动过期日志类型的数据

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值