【上节回顾】
在上一章节中,我们学习了MongoDB的基本概念,包括数据库、集合和文档。我们了解到MongoDB是一个开源、分布式的文档数据库,相比关系型数据库采用了不同的数据组织方式。我们明确了MongoDB中的数据库是集合的容器,集合是文档的容器,而文档是MongoDB中数据的基本单位,由键值对组成并以BSON格式存储。我们还学习了如何安装MongoDB,创建数据库和集合,以及在集合中插入和查询文档。这些基础知识为我们继续深入学习MongoDB打下了坚实的基础。本节课程,我们将更深入地了解BSON数据类型与文档结构,这是理解和有效使用MongoDB的关键。
【实战介绍】
本节课程是MongoDB实战课程第一部分"MongoDB基础入门"中的第二个章节。在理解了MongoDB的基本概念后,我们需要深入学习MongoDB使用的数据格式——BSON以及各种数据类型,这是有效操作MongoDB的基础。
本节课程主要讲解:
-
BSON格式及其与JSON的关系与区别
-
MongoDB支持的数据类型详解
-
文档结构设计与嵌套文档
-
数组操作与使用技巧
-
文档大小限制与处理方法
学习BSON数据类型与文档结构的价值在于:掌握这些知识可以帮助我们更精确地控制数据存储格式,优化数据访问性能,合理设计文档结构以适应业务需求,避免常见的文档设计陷阱。
【实战任务内容】
1. BSON格式简介
BSON(Binary JSON)是MongoDB使用的二进制格式,专为数据存储和网络传输而设计。
1.1 BSON与JSON的关系
BSON基于JSON格式,但进行了二进制编码,并增加了额外的数据类型支持。
示例:JSON格式的用户文档:
{
"name": "张三",
"age": 28,
"registered": true
}
转换为BSON后,虽然人眼看不到二进制形式,但它能更高效地存储,并支持更多数据类型。
1.2 BSON的优势
-
轻量级:快速遍历,减少存储空间
-
可遍历性:便于解析和创建
-
高效性:编码和解码速度快
-
额外数据类型:支持日期、二进制数据等JSON不支持的类型
示例:在BSON中,可以直接存储日期类型:
{
"name": "张三",
"birthday": new Date("1995-05-15"),
"created": ISODate("2023-10-20T08:00:00Z")
}
2. MongoDB数据类型
MongoDB支持多种数据类型,使数据存储更灵活精确。
2.1 基本数据类型
数据类型 | 描述 | 示例 |
---|---|---|
String | 字符串,UTF-8编码 | "name": "张三" |
Integer | 整数(32位/64位) | "age": 28 |
Double | 浮点数 | "score": 98.5 |
Boolean | 布尔值(true/false) | "active": true |
Null | 空值 | "middleName": null |
Date | 日期时间(毫秒精度) | "created": new Date() |
ObjectId | 12字节的ID | "_id": ObjectId("...") |
Array | 数组/列表 | "tags": ["mongodb", "database"] |
Embedded Document | 嵌入式文档 | "address": {"city": "北京"} |
Binary Data | 二进制数据 | "file": BinData(0, "...") |
示例:创建包含多种数据类型的文档:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "商品A",
"price": 199.99,
"inStock": true,
"category": ["电子", "配件"],
"details": {
"model": "X100",
"color": "黑色",
"weight": 120.5
},
"createdAt": new Date()
}
2.2 特殊数据类型
MongoDB还支持一些特殊数据类型,满足特定需求:
-
ObjectId:MongoDB自动为文档创建的唯一标识符
-
Timestamp:内部使用的时间戳类型
-
Regular Expression:正则表达式类型
-
JavaScript Code:存储JavaScript代码
-
MinKey/MaxKey:比较用的最小/最大键
示例:使用ObjectId和正则表达式:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "张三",
"email": "zhangsan@example.com",
"searchPattern": /^zhang/i
}
3. 文档结构设计
3.1 嵌入式文档
嵌入式文档是指文档内部包含的另一个文档,形成一种层次结构。
优点:
-
数据本地化,减少查询次数
-
原子操作保证一致性
-
适合一对一和一对少量多的关系
示例:用户文档中嵌入地址信息:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "李四",
"email": "lisi@example.com",
"address": {
"street": "建国路88号",
"city": "上海",
"postcode": "200120",
"country": "中国"
}
}
3.2 引用文档
引用文档通过存储其他文档的ID来建立关系,类似关系型数据库的外键。
优点:
-
避免数据重复
-
适合一对多和多对多关系
-
文档大小可控
示例:订单引用用户和产品:
// 用户文档
{
"_id": ObjectId("user123"),
"name": "王五",
"email": "wangwu@example.com"
}
// 产品文档
{
"_id": ObjectId("product456"),
"name": "智能手机",
"price": 2999
}
// 订单文档(引用用户和产品)
{
"_id": ObjectId("order789"),
"userId": ObjectId("user123"),
"items": [
{ "productId": ObjectId("product456"), "quantity": 1 }
],
"totalAmount": 2999,
"status": "已支付",
"createdAt": new Date()
}
4. 数组操作
MongoDB对数组类型提供强大支持,可以存储多个值并进行复杂查询。
4.1 数组基本操作
-
创建数组字段
-
添加、删除、更新数组元素
-
数组查询和过滤
示例:包含数组的文档:
{
"_id": ObjectId("article123"),
"title": "MongoDB数组操作指南",
"tags": ["数据库", "MongoDB", "NoSQL"],
"comments": [
{ "user": "用户A", "text": "很有帮助", "date": new Date() },
{ "user": "用户B", "text": "内容详细", "date": new Date() }
]
}
4.2 数组中的嵌入式文档
数组可以包含嵌入式文档,形成更复杂的数据结构。
示例:学生课程成绩记录:
{
"_id": ObjectId("student123"),
"name": "赵六",
"courses": [
{ "name": "数据库", "code": "DB101", "score": 92 },
{ "name": "编程基础", "code": "CS101", "score": 85 },
{ "name": "网络技术", "code": "NET101", "score": 88 }
],
"gpa": 3.8
}
5. 文档大小限制
MongoDB对文档大小有16MB的限制,这是为了保持性能和可管理性。
5.1 处理大文档的策略
-
使用引用代替嵌入
-
拆分文档
示例:拆分产品文档和产品详情:
// 产品基本信息
{
"_id": ObjectId("product123"),
"name": "高性能服务器",
"price": 9999,
"category": "电子设备"
}
// 产品详细信息
{
"_id": ObjectId("productDetail123"),
"productId": ObjectId("product123"),
"specifications": { /* 详细规格信息 */ },
"manual": { /* 使用手册内容 */ },
"reviews": [ /* 用户评价列表 */ ]
}
【实战操作】
实战1:使用MongoDB Shell查看和创建不同数据类型
首先,连接到MongoDB服务器:
root@ssdevops.com:~$ mongo
查看当前使用的数据库,如果需要,创建并切换到一个新数据库:
root@ssdevops.com:~$ show dbs
root@ssdevops.com:~$ use datatypes_demo
创建包含各种数据类型的文档:
root@ssdevops.com:~$ db.types_example.insertOne({
string_value: "这是一个字符串",
int_value: NumberInt(42),
long_value: NumberLong("9223372036854775807"),
double_value: 3.14159,
boolean_value: true,
null_value: null,
date_value: new Date(),
timestamp_value: new Timestamp(),
object_id: ObjectId(),
array_value: [1, 2, 3, "四", "五"],
embedded_doc: {
field1: "嵌入式文档字段1",
field2: 123
},
regex_value: /^M/i
})
查看插入的文档:
root@ssdevops.com:~$ db.types_example.find().pretty()
执行结果截图:
实战2:ObjectId详解与操作
查看ObjectId的结构和生成:
root@ssdevops.com:~$ var id = ObjectId()
root@ssdevops.com:~$ print("生成的ObjectId:", id)
root@ssdevops.com:~$ print("十六进制字符串:", id.toString())
root@ssdevops.com:~$ print("创建时间戳:", id.getTimestamp())
root@ssdevops.com:~$ print("创建时间:", new Date(id.getTimestamp()))
使用自定义ObjectId创建文档:
root@ssdevops.com:~$ var customId = ObjectId("507f1f77bcf86cd799439011")
root@ssdevops.com:~$ db.custom_ids.insertOne({
_id: customId,
description: "自定义ObjectId示例"
})
通过ObjectId查询文档:
root@ssdevops.com:~$ db.custom_ids.findOne({_id: ObjectId("507f1f77bcf86cd799439011")})
执行结果截图:
实战3:日期类型操作与查询
插入包含日期的文档:
root@ssdevops.com:~$ db.date_examples.insertMany([
{
title: "今天的文档",
created: new Date(),
updated: new Date(),
type: "current"
},
{
title: "昨天的文档",
created: new Date(new Date().setDate(new Date().getDate() - 1)),
updated: new Date(),
type: "recent"
},
{
title: "上周的文档",
created: new Date(new Date().setDate(new Date().getDate() - 7)),
updated: new Date(new Date().setDate(new Date().getDate() - 2)),
type: "old"
},
{
title: "去年的文档",
created: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
updated: new Date(new Date().setMonth(new Date().getMonth() - 6)),
type: "archive"
}
])
基于日期查询文档:
// 查找今天创建的文档
root@ssdevops.com:~$ var today = new Date()
root@ssdevops.com:~$ today.setHours(0, 0, 0, 0)
root@ssdevops.com:~$ var tomorrow = new Date(today)
root@ssdevops.com:~$ tomorrow.setDate(tomorrow.getDate() + 1)
root@ssdevops.com:~$ db.date_examples.find({
created: {
$gte: today,
$lt: tomorrow
}
}).pretty()
// 查找一周内更新的文档
root@ssdevops.com:~$ var weekAgo = new Date()
root@ssdevops.com:~$ weekAgo.setDate(weekAgo.getDate() - 7)
root@ssdevops.com:~$ db.date_examples.find({
updated: {
$gte: weekAgo
}
}).pretty()
// 按创建日期排序
root@ssdevops.com:~$ db.date_examples.find().sort({created: -1}).pretty()
执行结果截图:
实战4:数组操作技巧
创建包含数组的文档集合:
root@ssdevops.com:~$ db.array_examples.insertMany([
{
name: "产品A",
tags: ["电子", "智能", "家用"],
ratings: [4, 5, 3, 5, 4]
},
{
name: "产品B",
tags: ["办公", "电子", "配件"],
ratings: [3, 3, 2, 4]
},
{
name: "产品C",
tags: ["家用", "厨房", "电器"],
ratings: [5, 5, 4, 5]
}
])
数组查询操作:
// 查找包含特定标签的产品
root@ssdevops.com:~$ db.array_examples.find({tags: "电子"}).pretty()
// 查找同时包含多个标签的产品
root@ssdevops.com:~$ db.array_examples.find({tags: {$all: ["电子", "家用"]}}).pretty()
// 使用$in操作符(查找包含任一标签的产品)
root@ssdevops.com:~$ db.array_examples.find({tags: {$in: ["家用", "办公"]}}).pretty()
// 按数组大小查询
root@ssdevops.com:~$ db.array_examples.find({tags: {$size: 3}}).pretty()
// 查找评分中包含5分的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: 5}).pretty()
// 查找所有评分均大于3的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: {$gt: 3}}).pretty()
// 查找任意评分大于4的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: {$elemMatch: {$gt: 4}}}).pretty()
数组更新操作:
// 添加新元素到数组中
root@ssdevops.com:~$ db.array_examples.updateOne(
{name: "产品A"},
{$push: {tags: "畅销"}}
)
// 一次添加多个元素
root@ssdevops.com:~$ db.array_examples.updateOne(
{name: "产品B"},
{$push: {tags: {$each: ["新品", "促销"]}}}
)
// 从数组中移除元素
root@ssdevops.com:~$ db.array_examples.updateOne(
{name: "产品C"},
{$pull: {tags: "厨房"}}
)
// 更新数组中的特定元素
root@ssdevops.com:~$ db.array_examples.updateOne(
{name: "产品A", ratings: 3},
{$set: {"ratings.$": 4}}
)
// 查看更新结果
root@ssdevops.com:~$ db.array_examples.find().pretty()
执行结果截图:
实战5:嵌入式文档与引用操作
创建使用嵌入式文档的集合:
root@ssdevops.com:~$ db.embedded_example.insertMany([
{
name: "张三",
contact: {
email: "zhangsan@example.com",
phone: "13800138000",
address: {
city: "北京",
district: "海淀区",
street: "中关村大街1号"
}
},
hobbies: [
{name: "读书", years: 5},
{name: "摄影", years: 2}
]
},
{
name: "李四",
contact: {
email: "lisi@example.com",
phone: "13900139000",
address: {
city: "上海",
district: "浦东新区",
street: "张江高科技园区"
}
},
hobbies: [
{name: "游泳", years: 3},
{name: "钢琴", years: 7}
]
}
])
嵌入式文档查询:
// 嵌入式文档完全匹配
root@ssdevops.com:~$ db.embedded_example.find({
"contact.address.city": "北京"
}).pretty()
// 多层嵌入式文档查询
root@ssdevops.com:~$ db.embedded_example.find({
"contact.address.district": "浦东新区"
}).pretty()
// 查询数组中的嵌入式文档
root@ssdevops.com:~$ db.embedded_example.find({
"hobbies.name": "摄影"
}).pretty()
// 使用$elemMatch操作符进行复合条件查询
root@ssdevops.com:~$ db.embedded_example.find({
hobbies: {
$elemMatch: {
name: "钢琴",
years: {$gt: 5}
}
}
}).pretty()
创建使用引用关系的集合:
// 用户集合
root@ssdevops.com:~$ db.users.insertMany([
{
name: "王五",
email: "wangwu@example.com"
},
{
name: "赵六",
email: "zhaoliu@example.com"
}
])
// 商品集合
root@ssdevops.com:~$ db.products.insertMany([
{
name: "笔记本电脑",
price: 5999,
stock: 10
},
{
name: "智能手机",
price: 2999,
stock: 20
}
])
// 订单集合(引用用户和商品)注意:以下引用的ObjectId是上边插入用户和商品时生成的id,需要按实际修改
root@ssdevops.com:~$ db.orders.insertOne({
userId: ObjectId("682fe3e8cb9f47e1bd166701"),
orderDate: new Date(),
items: [
{
productId: ObjectId("682fe3ffcb9f47e1bd166703"),
quantity: 1,
price: 5999
},
{
productId: ObjectId("682fe3ffcb9f47e1bd166704"),
quantity: 2,
price: 2999
}
],
totalAmount: 11997,
status: "已付款"
})
使用手动连接查询引用关系:
// 查找订单
root@ssdevops.com:~$ var order = db.orders.findOne()
// 查找订单关联的用户
root@ssdevops.com:~$ var user = db.users.findOne({_id: order.userId})
root@ssdevops.com:~$ print("订单用户:", user.name)
// 查找订单包含的商品
root@ssdevops.com:~$ order.items.forEach(function(item) {
var product = db.products.findOne({_id: item.productId})
print("商品:", product.name, "数量:", item.quantity)
})
执行结果截图:
实战6:BSON数据转换操作
在MongoDB Shell中进行BSON/JSON转换:
// 创建一个包含各种类型的文档
root@ssdevops.com:~$ var complexDoc = {
_id: ObjectId(),
name: "BSON测试",
date: new Date(),
binary: BinData(0, "VGhpcyBpcyBhIHRlc3Q="),
nested: {
field1: 123,
field2: "嵌套字段"
},
array: [1, 2, 3, {subField: "test"}],
numberLong: NumberLong("9223372036854775807"),
numberInt: NumberInt(123)
}
// 插入文档
root@ssdevops.com:~$ db.bson_test.insertOne(complexDoc)
// 转换为JSON格式
root@ssdevops.com:~$ JSON.stringify(complexDoc)
// 转换为松散扩展的JSON格式(包含类型信息)
root@ssdevops.com:~$ printjson(complexDoc)
// 转换为严格模式的JSON(会丢失一些BSON特定类型信息)
root@ssdevops.com:~$ var strict = JSON.parse(JSON.stringify(complexDoc))
root@ssdevops.com:~$ printjson(strict)
查看BSON特定类型如何在JSON中表示:
// ObjectId在JSON中的表示
root@ssdevops.com:~$ print("ObjectId在JSON中:", JSON.stringify({id: ObjectId()}))
// Date在JSON中的表示
root@ssdevops.com:~$ print("Date在JSON中:", JSON.stringify({date: new Date()}))
// NumberLong在JSON中的表示
root@ssdevops.com:~$ print("NumberLong在JSON中:", JSON.stringify({long: NumberLong("9223372036854775807")}))
注意BSON到JSON的转换限制:
root@ssdevops.com:~$ print("BSON转JSON可能会丢失类型信息,特别是对于日期、二进制数据和特殊数字类型")
执行结果截图:
【课后思考】
-
在设计MongoDB文档结构时,什么情况下应该选择嵌入式文档,什么情况下应该选择文档引用?考虑数据访问模式、更新频率和数据规模。
-
MongoDB的BSON格式相比JSON格式有哪些优势和局限性?如何在实际应用中最大化利用BSON的特性?
-
对于一个社交媒体应用,如何设计用户、帖子和评论的文档结构?考虑查询效率、数据完整性和可扩展性。
【高频面试题】
面试题1:解释MongoDB中BSON和JSON的区别,以及为什么MongoDB选择BSON作为存储格式?
思路解析: BSON(Binary JSON)和JSON(JavaScript Object Notation)的主要区别在于:
-
数据格式:JSON是一种文本格式,易于人类阅读;而BSON是二进制格式,计算机处理更高效。
-
数据类型支持:JSON支持的数据类型有限(字符串、数字、布尔值、数组、对象、null);BSON扩展了更多数据类型,如日期、二进制数据、正则表达式等。
-
性能表现:BSON在编码和解码速度上优于JSON,特别是对于数字和二进制数据。
-
空间效率:对于某些数据(特别是数字),BSON更节省空间;但对于字符串,BSON可能占用更多空间,因为存储了字符串长度。
MongoDB选择BSON作为存储格式的原因:
-
遍历效率高:BSON设计便于快速扫描
-
编码/解码速度快:对数据库性能至关重要
-
类型丰富:支持日期、二进制等数据库常用类型
-
字段长度前置:使解析更高效
-
可扩展性:便于添加新的数据类型和功能
在实际应用中,MongoDB在内部使用BSON存储,但对外提供JSON接口,实现了两者的优势结合。用户可以使用易读的JSON格式与数据库交互,而MongoDB内部则利用BSON的高效性能进行存储和处理。这种设计使MongoDB既保持了开发友好性,又获得了高性能的数据处理能力。
面试题2:在MongoDB中,如何处理超过16MB文档大小限制的数据?各种解决方案的优缺点是什么?
思路解析: MongoDB对单个文档的大小限制为16MB,处理超过此限制的数据有以下几种方案:
-
文档引用(Document References)
-
方法:将大文档拆分为多个相关文档,通过ID引用关联
-
优点:简单直观、维护引用关系清晰
-
缺点:需要多次查询才能获取完整数据,应用层需要处理连接逻辑
-
-
分桶策略(Bucketing)
-
方法:按一定规则(如时间段)将相关数据分组到多个文档中
-
优点:同时获取一组相关数据比较高效
-
缺点:桶的设计需要仔细考虑以平衡性能和数据组织
-
-
数据压缩
-
方法:在应用层对数据进行压缩后存储
-
优点:减少存储空间、可能允许更多数据放入单个文档
-
缺点:额外的CPU开销、不便于查询和更新部分内容
-