在上一部分,我们对 MongoDB 有了一个直观的初步认识。现在,让我们一起正式进入 MongoDB 的学习阶段,逐步揭开它强大与灵活的面纱。

在 MongoDB 中,「文档(Document)」是数据存储的基本单元。每个文档以类 JSON(实际为 BSON)格式保存,能够灵活地包含各种类型的数据,如字符串、数值、数组、甚至嵌套文档。这种灵活的数据结构使得 MongoDB 能够高效地适应各种业务场景与数据模型需求。 一个基础的文档示例如下:
|{"问候语": "你好,世界!"}
这个文档包含了一个键值对,其中「问候语」是键,「你好,世界!」是对应的值。当然,实际应用中的文档通常会更加复杂,包含多个键值对:
|{ "商品名称": "智能手机", "价格": 2999, "库存": 150, "上架时间": "2024-01-15" }
在 MongoDB 中,文档的键(Key)必须为字符串类型,并支持任意 UTF-8 编码字符。需要特别注意,键名称中禁止包含空字符(\0),因为该字符用于标识字符串的结束。
此外,点号(.)和美元符号($)在 MongoDB 内部具有特殊语义,一般不建议在自定义键中出现。如果滥用这些字符,可能会导致存储或检索行为异常,甚至触发数据库内部错误。
请牢记:MongoDB 对于字段的数据类型和字段名的大小写都是严格区分的。例如,{"数量": 5} 与 {"数量": "5"} 被视为结构和内容完全不同的文档。同样,{"name": "张三"} 与 {"Name": "张三"} 也不会被归为同一个字段。开发时需仔细核查字段类型及名称,避免数据混乱或查询失效。
值得强调的是,MongoDB 不允许同一文档中存在重复的键。出现如下重复键会导致文档不合法,系统将拒绝写入:
|{"用户名": "小明", "用户名": "小红"} // 这是错误的
在 MongoDB 中,「集合(Collection)」是存储文档的逻辑容器,其作用等同于关系型数据库中的表(Table)。与传统数据库表的固定结构不同,MongoDB 的集合采用灵活的动态模式,允许同一集合内存储结构各异的文档,从而极大地提升了系统对多样化数据模型的适应能力。
在关系型数据库中,表的每一行(记录)都必须严格遵循预定义的模式(Schema),即字段类型和数量均需保持一致。而 MongoDB 的集合则允许每个文档拥有不同的字段集与数据类型,这种设计为数据模型的迭代和拓展提供了极大的灵活性。例如,在同一个「用户信息」集合中,可以并存如下结构不同的两个文档:
|{"用户名": "张三", "年龄": 25, "职业": "程序员"} {"姓名": "李四", "生日": "1990-05-15", "爱好": ["阅读", "游泳"]}
这两个文档的结构完全不同,但它们可以和谐地共存在同一个集合中。 那么问题来了:既然可以把所有文档都放在一个集合里,为什么还要创建多个集合呢?实际上,合理地分离不同类型的文档有很多好处。
首先是管理上的便利性。想象一下,如果你要查找所有的商品信息,但这些信息和用户评论混在一起,你需要在成千上万的文档中筛选出商品相关的内容,这会让查询变得复杂且低效。 其次是性能考虑。当文档类型相似时,MongoDB 可以更好地优化存储和查询。就像图书馆按照类别分区存放图书一样,相关的文档聚集在一起可以减少磁盘查找次数,提升访问速度。 最后是索引效率。MongoDB 的索引是针对集合创建的,当集合中的文档类型一致时,索引的效果会更好。
在 MongoDB 中,集合名称必须为有效的 UTF-8 字符串,并遵循以下规则以确保数据一致性与系统稳定性:
\0),因为空字符用于字符串终止。此外,MongoDB 采用点号(.)作为命名空间分隔符,支持「子集合」的命名习惯。例如,可按业务模块划分为「blog.articles」、「blog.comments」等子集合。需要注意的是,这种分隔仅限于命名约定,并不会实质性地建立集合之间的层级或父子关系,但有助于大型应用的数据归类与项目结构优化。
数据库(Database)是 MongoDB 中的数据管理的顶层逻辑隔离单元,用于有序地组织与管理相关的集合。一个 MongoDB 实例可同时承载多个数据库,每个数据库内部包含多个集合,每个集合内存储若干文档,实现数据的分级与归属。 通常建议将单一应用的全部业务数据集中存放于同一个数据库,以便统一管理与维护。当存在多应用共用一套 MongoDB 实例,或需为不同租户、用户实现物理隔离时,可按需划分为多个数据库,从而提升安全性与数据独立性。
数据库命名需遵循如下规则:名称不能为空字符串,且不得包含斜杠(/)、反斜杠(\)、点号(.)等特殊字符。数据库名称对大小写不敏感,但建议全局采用统一风格以保证可维护性。同时,名称长度不得超过 64 字节,否则会被系统拒绝。
此外,MongoDB 预定义了若干特殊用途的数据库:
admin:超级库,承担用户认证、权限分配及集群范围内管理操作,部分管理命令须在该数据库下执行。local:本地库,主要保存实例级别的元数据及本地持久化信息。在副本集环境下,还用于存储复制所需的中间数据,不参与节点间同步。config:仅在分片集群架构中存在,负责集中存储分片元数据及集群路由等全局配置信息。正确使用数据库及合理命名,有助于提升系统的管理效率与数据安全性。
在正式开始 MongoDB 学习之前,我们需要确保 MongoDB 已经安装在我们的系统中。如果你还没有安装 MongoDB,可以访问 MongoDB 官方网站下载适合你操作系统的版本。MongoDB 提供了 Windows、macOS 和 Linux 多个平台的安装包,按照官方文档的指引一步步安装即可。
当然,如果你暂时不想在本地安装 MongoDB,也可以使用一些优秀的在线编辑器来进行学习。例如 One Compiler 就提供了 MongoDB 的在线环境,让你可以在浏览器中直接体验 MongoDB 的各种操作,非常适合初学者快速上手。
下面我们使用本地的MongoDB来启动 MongoDB 服务器。一旦 MongoDB 安装完成,开始使用 MongoDB 的第一步是启动 MongoDB 服务器。在命令行中运行 mongod 命令即可:
|$ mongod
启动成功后,你会看到类似这样的输出信息:
|MongoDB starting : pid=8680 port=27017 dbpath=/data/db 64-bit db version v4.2.0 waiting for connections on port 27017
这些信息告诉我们 MongoDB 服务器已经成功启动,正在 27017 端口上等待连接。
首次启动 MongoDB 时,请确保数据目录(默认是 /data/db/)已经存在且有写入权限。如果目录不存在或没有权限,服务器将无法启动。
MongoDB Shell 是一个功能强大的命令行工具,它不仅是数据库的管理界面,还是一个完整的 JavaScript 解释器。通过 Shell,我们可以与 MongoDB 进行各种交互操作。
启动 Shell 很简单,在另一个命令行窗口中输入:
|$ mongo
Shell 会自动尝试连接到本地运行的 MongoDB 服务器:
|MongoDB shell version: 4.2.0 connecting to: test >
现在你已经连接到了「test」数据库。Shell 将这个数据库连接赋值给了全局变量「db」,你可以通过输入 db 来查看当前连接的数据库:
|> db test
如果你想切换到其他数据库,可以使用 use 命令,例如切换到「商店管理系统」数据库(当然我们还没有创建这个数据库,不过没关系,我们后续会学到怎么创建一个属于我们自己的数据库):
|> use 商店管理系统 switched to db 商店管理系统
MongoDB Shell 的一个独特之处在于它是一个完整的 JavaScript 解释器。这意味着你可以进行各种计算和操作:
|> x = 200; 200 > x / 5; 40 > Math.sin(Math.PI / 2); 1 > new Date(); ISODate("2025-01-15T10:30:00Z")
你甚至可以定义和调用 JavaScript 函数:
|> function 计算阶乘(n) { ... if (n <= 1) return 1; ... return n * 计算阶乘(n - 1); ... } > 计算阶乘(5); 120

MongoDB 的核心操作被统称为 CRUD,即创建(Create)、读取(Read)、更新(Update)和删除(Delete)。下面我们通过实际示例,对这些基本操作进行讲解。
以电影数据库为例,首先定义一个电影文档:
|电影 = { "片名": "流浪地球", "导演": "郭帆", "上映年份": 2019, "类型": ["科幻", "灾难"] }
然后使用 insertOne 方法将这个文档保存到「电影」集合中:
|db.电影.insertOne(电影) # 输出 { "acknowledged": true, "insertedId": ObjectId("507f1f77bcf86cd799439011") }
要检索刚刚插入的文档,可以使用 find 方法进行查询:
|db.电影.find().pretty() # 输出 { "_id" : ObjectId("68f6e83d615c44cefac07225"), "片名" : "流浪地球", "导演" : "郭帆", "上映年份" : 2019, "类型" : [ "科幻", "灾难" ] }
可以观察到,文档中新增了一个「_id」字段,这是由 MongoDB 自动分配的全局唯一标识符,用于唯一确定每一条记录。 如需检索单个文档,可以使用 findOne 方法:
|db.电影.findOne()
我们还可以根据条件查找文档:
|> db.电影.findOne({"导演": "郭帆"})
如果我们需要为该电影文档添加评分及相关信息,可采用 updateOne 方法进行更新操作:
|db.电影.updateOne( {"片名": "流浪地球"}, {$set: {"评分": 8.5, "评论数": 2850}} ) # 输出 { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
第一个参数是查找条件,第二个参数使用了 $set 操作符来指定要更新的字段。
如需删除文档,可使用 deleteOne 或 deleteMany 方法:
|db.电影.deleteOne({"片名": "流浪地球"})
删除操作是不可恢复的,执行前必须充分确认操作对象和条件。特别是在使用 deleteMany 方法时,应谨慎设置过滤条件,以防止误删大量数据造成无法挽回的损失。
上面四个操作是MongoDB中最为基础的操作,也是我们后续使用的最多的操作,在我们进行下一步的学习之前,让我们现在看看在MongoDB中有哪些常见的数据类型吧。不用担心你记不住,下一节我们将重新回到这几个最基础的操作部分做更多的学习。
在深入理解 MongoDB 的数据存储机制之前,我们必须先掌握它所支持的丰富数据类型体系。MongoDB 采用了 BSON(Binary JSON)格式作为其数据存储的标准,这种二进制序列化格式不仅完全兼容 JSON,还在此基础上扩展了更多实用的数据类型,为开发者提供了更强大的数据表达能力。

空值类型在 MongoDB 中用 null 来表示,它主要用于处理那些暂时没有值或者明确表示为空的字段情况。在实际的业务场景中,当某个字段处于未初始化状态或者需要显式表达“无值”的概念时,null 类型就显得尤为重要。 例如,在一个用户信息文档中,如果用户还没有填写个人简介,我们就可以使用 null 来占位:
|{ "用户名": "张三", "年龄": 28, "个人简介": null, "注册时间": "2024-01-15" }
这种设计让数据模型更加灵活,既能表示字段的存在,又能准确表达其当前为空的状态。
布尔值类型只有两个可能的值:true 和 false,它在 MongoDB 中扮演着重要的角色,广泛应用于各种状态标识和条件判断的场景。从商品是否上架、用户账号是否激活、订单是否已支付,到系统配置的开关状态,都可以用布尔值来进行精确的逻辑表达。 考虑这样一个电商场景的文档:
|{ "商品名称": "智能手机", "价格": 2999, "是否上架": true, "是否促销": false, "支持分期": true }
布尔值的简洁性和明确性让它成为状态管理的最佳选择,同时也能显著提升查询条件的编写效率。
数字类型在 MongoDB 中的处理相对复杂,这是因为它需要平衡数值精度、存储效率和计算性能之间的关系。在 MongoDB Shell 中,默认情况下所有数字都会被存储为 64 位双精度浮点数,这种设计提供了极大的数值范围(大约 ±1.8×10^308)和较高的精度(约15位有效数字)。
当我们处理价格、重量、百分比等需要小数精度的数值时,这种默认的浮点数类型通常都能很好地满足需求:
|{ "商品价格": 2999.99, "折扣率": 0.85, "重量": 156.7, "评分": 4.8 }
然而,在处理整数运算或者需要精确表示大整数的场景中,我们就需要特别注意数据类型的选择。MongoDB 提供了专门的整数类型来应对这些需求:NumberInt 用于 32 位整数,NumberLong 用于 64 位大整数。 比如在处理文档页数、用户计数等场景时:
|{ "书籍页数": NumberInt("256"), "网站访问量": NumberLong("1234567890"), "库存数量": NumberInt("500") }
这种精确的类型控制确保了数值计算的准确性,避免了浮点数运算可能带来的精度误差。
字符串类型是 MongoDB 中使用频率最高的数据类型之一,它能够存储任何有效的 UTF-8 字符序列,包括中文、英文、表情符号等各种字符。这种设计让 MongoDB 能够完美支持国际化应用的需求。 在用户信息、商品描述、评论内容等文本密集的字段设计中,字符串类型都是当之无愧的首选:
|{ "用户名": "张小明", "商品描述": "这款智能手机采用先进的处理器技术,配备高清摄像头,续航能力卓越,为您的数字生活提供全方位的支持。", "用户评价": "非常满意!运行流畅,拍照效果惊艳!五星好评!👍" }
字符串类型的灵活性不仅体现在字符集的支持上,还在于它能够与 MongoDB 的全文搜索功能完美配合,为复杂的文本检索需求提供强大的支持。
日期类型是处理时间相关数据的重要工具,它在 MongoDB 中以自 Unix 纪元(1970年1月1日 00:00:00 UTC)以来的毫秒数形式进行存储。这种存储方式确保了跨平台的时间一致性,并提供了精确到毫秒的时间精度。 通过 JavaScript 的 Date 对象,我们可以方便地创建和操作日期:
|{ "创建时间": new Date(), "订单时间": new Date("2024-01-15T10:30:00Z"), "最后登录": ISODate("2024-12-01T08:00:00.000Z"), "到期时间": new Date("2025-12-31") }
这种基于毫秒的时间戳存储方式,不仅保证了时间数据的精确性,还为基于时间范围的查询和索引优化提供了便利。
数组类型为 MongoDB 提供了存储有序数据集合的能力,一个数组可以包含多个不同类型的值,这种灵活的设计让数组成为处理列表、标签、选项等数据的理想选择。在实际应用中,无论是商品标签、用户兴趣爱好,还是订单中的商品列表,都可以通过数组类型来完美表达。 数组的强大之处在于它的异构性:
|{ "商品标签": ["热门", "推荐", "新品"], "用户兴趣": ["阅读", "运动", "音乐", "旅行"], "混合数据": ["文本描述", 123, true, null], "购物车": [ {"商品ID": "P001", "数量": 2, "价格": 99.00
这种设计不仅支持简单的字符串数组,还能存储复杂的嵌套对象,为数据建模提供了极大的灵活性。同时,MongoDB 还提供了丰富的数组查询操作符,让开发者能够高效地处理数组中的数据。
嵌入文档是 MongoDB 最具革命性的特性之一,它允许我们在文档内部嵌套其他文档,完全打破了传统关系型数据库的范式限制。这种设计让我们能够在单个文档中完整地表达复杂的对象关系,为数据查询和更新的原子性操作提供了可能。 在用户信息管理中,我们可以将地址信息完整地嵌入到用户文档中:
|{ "用户名": "李四", "年龄": 32, "联系方式": { "手机": "13800138000", "邮箱": "lisi@example.com", "微信": "lisi2024" }, "地址": { "街道": "中山路123号", "城市": "上海", "邮编": "200000", "坐标": {
嵌入文档的优势主要体现在查询效率和数据一致性方面。由于相关数据存储在同一个文档中,我们可以通过一次查询获得完整的信息,同时也避免了多表关联查询的复杂性。 当然,嵌入文档也不是万能的。在设计时,我们需要考虑文档大小不能超过 16MB 的限制,还要根据实际的查询模式和数据访问频率来决定是否使用嵌入还是引用。在处理一对多关系时,嵌入文档往往是最佳选择;而对于多对多关系或者数据经常独立更新的场景,引用设计可能更加合适。
MongoDB 中每个文档都包含唯一的 _id 字段,确保在集合内的唯一性。如果未显式指定,系统会自动生成 12 字节的 ObjectId,其结构结合了时间戳、机器/进程标识与自增计数器,能够在分布式高并发环境下高效、安全地生成全局唯一标识,并便于基于时间的查询优化。
实际应用中可以选择自定义 _id(如使用邮箱、订单号等自然主键),也可交由 MongoDB 自动生成,但必须保证其唯一性,否则插入会失败。
MongoDB Shell 支持连接本地及远程部署的 MongoDB 数据库实例,便于集中管理和运维:
|$ mongo 服务器地址:端口号/数据库名
例如:
|$ mongo production-server:27017/商店数据库
MongoDB Shell 支持执行 JavaScript 脚本文件,能够实现复杂的批量处理和自动化运维任务:
|$ mongo 数据初始化.js 数据清理.js
在 MongoDB Shell 的交互模式下,可通过 load 函数高效加载并执行外部 JavaScript 脚本,实现批量化或自动化的数据处理操作:
|> load("数据处理脚本.js")
可以通过自定义 Shell 的提示符,使其展示更多关键信息以提升操作效率:
|prompt = function() { return new Date().toLocaleTimeString() + " [" + db + "]> "; };
这会让提示符显示当前时间和数据库名称:
|prompt() #输出 02:15:45 [db_4423ncbta_44297pv68]>
在某些情况下,集合名称可能包含特殊字符、以数字开头,或与 JavaScript 关键字产生冲突。为了保证对此类集合的安全、规范访问,推荐使用 getCollection 方法:
|> db.getCollection("123数字开头的集合") > db.getCollection("带-连字符-的集合")
也可以使用数组语法:
|> db["特殊集合名称"]
假设我们有一个「学生」集合,需要存储学生的基本信息。请根据以下数据创建一个学生文档并插入到数据库中:
原始数据:
|学生信息 = { "姓名": "王小明", "年龄": 18, "专业": "计算机科学", "成绩": { "数学": 85, "英语": 92, "编程": 88 }, "是否注册": true }
任务: 使用 insertOne 方法将这个学生信息插入到「学生」集合中。
|db.学生.insertOne({ "姓名": "王小明", "年龄": 18, "专业": "计算机科学", "成绩": { "数学": 85, "英语": 92, "编程": 88 }, "是否注册": true })
基于练习1中插入的学生数据,请查询王小明的编程成绩。
任务: 使用 findOne 方法查询王小明的编程成绩信息。
|db.学生.findOne({"姓名": "王小明"}, {"成绩.编程": 1, "姓名": 1})
继续使用练习1中的学生数据,我们需要为王小明添加联系方式信息。
新数据:
|联系方式 = { "手机": "13800138001", "邮箱": "wangxiaoming@example.com" }
任务: 使用 updateOne 方法为王小明添加联系方式信息。
|db.学生.updateOne( {"姓名": "王小明"}, {$set: { "联系方式": { "手机": "13800138001", "邮箱": "wangxiaoming@example.com" } }} )