第1章 认识 MongoDB 与环境搭建
🎯 本章学习目标
学完本章,你应当能够:
- 说清 MongoDB 的定位,以及文档模型 vs 关系模型各自擅长什么、什么时候该换 MongoDB。
- 背下 MongoDB 与 MySQL 的概念对照表:数据库/集合/文档/字段 ↔ 库/表/行/列,
_id/ObjectId ↔ 主键。 - 理解 BSON 文档结构、
_id唯一索引、无固定 schema 的含义。 - 在 Windows(MSI)或 Docker 上安装 MongoDB 8.x,用 mongosh 和 Compass 验证。
- 用 Java Driver 跑通第一次连接,登记后续章节要用的连接信息。
1.1 MongoDB 是什么:给 MySQL 同学的三分钟背景
MongoDB 是一款开源的文档型 非关系型( NoSQL) 数据库,它把数据存成类似 JSON 的“文档”,而不是像关系型数据库那样存成行列分明的二维表。官方手册的原话是:MongoDB 将数据存储在类似 JSON 的灵活文档中,从而可以像应用程序代码使用数据一样轻松地对数据进行建模。
- 主流版本:MongoDB 8.x(2024 起的主流发布线),学习用 Community 版免费;云上可直接用 MongoDB Atlas 免费 M0 集群,零安装。
- 核心差异:MySQL 存「行」,MongoDB 存「文档」——一个文档就是一个自包含的 JSON 对象,把相关数据聚在一起。
- 典型场景:商品目录(属性各异)、内容管理(文章+评论)、用户画像(标签集合)、订单(主单+多明细)、IoT 时序、移动/游戏后端。
- 扩展性:原生分片(sharding)水平扩容,是它相对 MySQL 单机架构的杀手锏。
一句话认知
把 MongoDB 想成「用 JSON 存数据、按文档整块读写、天生好扩展的数据库」。你有 MySQL 的底子,学习重点不是「语法」,而是建模思维从「拆表」转向「聚合文档」——这是第 3 章的核心。
1.2 概念对照:MySQL 旧知识如何在 MongoDB「对号入座」
这是 MySQL 程序员转 MongoDB 最重要的一张表,贯穿全程,建议贴在显示器边。
| MySQL | MongoDB | 差异要点(踩坑预警) |
|---|---|---|
| Database 数据库 | Database 数据库 | 概念相同;库和集合都首次写入时自动创建,不用 CREATE DATABASE |
| Table 表 | Collection 集合 | 集合无固定 schema:同一个集合里每行字段可以不一样 |
| Row 行 | Document 文档 | 文档是 BSON 对象,一行 = 一个自包含 JSON,可内嵌数组/子文档 |
| Column 列 | Field 字段 | 字段可有可无、类型可变(一个文档 age 是数字,另一个可以是字符串——不建议但允许) |
| 主键 PRIMARY KEY | _id 字段 | 每个文档必有一个唯一的 _id,默认自动生成 ObjectId;_id 上自动建唯一索引 |
AUTO_INCREMENT | ObjectId / 自增序列 | ObjectId 是 12 字节全局唯一 ID(含时间戳),无需中心化发号,天然适合分布式 |
SQL SELECT * FROM t WHERE ... | MQL db.collection.find(...) | MongoDB 用「方法调用 + 查询对象」表达,不是 SQL 文本 |
| JOIN 联表 | $lookup / 内嵌文档 | MongoDB 弱关联建模;能内嵌就别 JOIN(第 3、5 章) |
information_schema | listCollections / $jsonSchema 校验 | schema 灵活,元数据概念不同 |
| 默认端口 3306 | 默认端口 27017 | 连接串、防火墙要记牢 |
root 用户 | 无内置 root;自建 admin 用户 | 鉴权默认关闭(本地开发),生产要开 auth(第 8 章) |
binlog | oplog | 副本集同步靠 oplog,思路类似 binlog(第 8 章) |
| InnoDB 引擎 | WiredTiger(默认,唯一生产引擎) | 不用选引擎,默认即 WiredTiger |
mysqldump | mongodump / mongorestore | 逻辑备份,第 8 章 |
| B+Tree 索引 | B-Tree 索引 | 索引思想高度相似(第 4 章),经验可大量复用 |
| 事务(强) | 单文档原子 + 多文档事务(4.0+,副本集) | 事务能力有,但心智不同(第 6 章) |
面试爱问:MongoDB 到底是不是「没有表结构」?
不是「没有结构」,而是「结构不强制、可变」。集合默认不校验字段,任意文档可增删字段;但你可以用 $jsonSchema 给集合加校验规则(第 3 章),把 MySQL 的「强 schema」找回来。答题时点出「灵活 schema ≠ 无 schema」是加分项。
1.2.1 一个文档长什么样
对照 MySQL「一行」,看 MongoDB「一个文档」(这里是一条商品):
{
"_id": ObjectId("665f1a2b9c1d4e5f6a7b8c9d"),
"title": "预见猿份 Java 实战课程",
"price": 199.00,
"tags": ["Java", "面试", "实战"],
"stock": { "total": 100, "locked": 3 },
"skus": [
{ "skuId": "A-001", "spec": "电子版", "price": 199.00 },
{ "skuId": "A-002", "spec": "纸质版", "price": 249.00 }
],
"createTime": ISODate("2026-01-01T10:00:00Z")
}MySQL 要表达这条数据,得拆成 商品表 + 标签表 + SKU表 + 库存表 四张表 JOIN;MongoDB 一个文档全装下——这就是「文档」二字的分量,也是建模的出发点。
这条文档怎么写进库里?mongosh 与 Java 两种外壳对照(后续所有章节的示例都会这样成对给出):
// mongosh:一条命令直接插入
const product = {
title: "预见猿份 Java 实战课程",
price: 199.00,
tags: ["Java", "面试", "实战"],
stock: { total: 100, locked: 3 },
skus: [
{ skuId: "A-001", spec: "电子版", price: 199.00 },
{ skuId: "A-002", spec: "纸质版", price: 249.00 }
],
createTime: new Date("2026-01-01T10:00:00Z")
};
db.t_product.insertOne(product);// Java Driver:Document 就是可嵌套的 Map,结构一一对应
Document product = new Document("title", "预见猿份 Java 实战课程")
.append("price", 199.00)
.append("tags", List.of("Java", "面试", "实战"))
.append("stock", new Document("total", 100).append("locked", 3))
.append("skus", List.of(
new Document("skuId", "A-001").append("spec", "电子版").append("price", 199.00),
new Document("skuId", "A-002").append("spec", "纸质版").append("price", 249.00)))
.append("createTime", new Date());
db.getCollection("t_product").insertOne(product);仔细观察:mongosh 里的
{键: 值}对象,与 Java 里的new Document(键, 值),装的是同一个 BSON 结构——这正是本书「一种操作、两种写法」互译的基础。
1.2.2 BSON 类型速览(对照 MySQL 列类型)
| BSON 类型 | 说明 | MySQL 对应 |
|---|---|---|
Double / Int32 / Int64 / Decimal128 | 数值 | DECIMAL / INT / BIGINT(金额推荐 Decimal128) |
String | UTF-8 字符串 | VARCHAR/TEXT |
Object / Array | 子文档 / 数组 | 无对应,正是文档模型的杀手锏 |
Boolean | 布尔 | TINYINT(1) |
Date (ISODate) | 日期时间(毫秒) | DATETIME/TIMESTAMP |
ObjectId | 12 字节唯一 ID | 自增主键的分布式替代 |
Null / Undefined | 空 / 未定义 | NULL |
1.3 安装 MongoDB 8.x
1.3.1 Windows 安装(MSI,学习推荐)
注意:MongoDB 新版仅支持 64 位 Windows Server 2022 / Windows 11,且不支持 WSL 跑 mongod。老系统建议改用下面的 Docker 方式。
- 从 MongoDB Download Center 选 Community Server 8.x + Windows x64 MSI 下载。
- 运行 MSI,安装类型选 Complete,安装到默认
C:\Program Files\MongoDB\Server\8.x\。 - 配置 Service 页勾选 "Run as Network Service"(或指定数据/日志目录),安装器会自动把
mongod注册成 Windows 服务并开机自启。 - 数据默认在
C:\Program Files\MongoDB\Server\8.x\data\db,日志在logs\mongod.log。 mongosh(命令行)与Compass(图形)需单独下载安装,不随 Server 自带。
验证服务:sc query MongoDB 或在「服务」里看 MongoDB 是否 Running。
1.3.2 Docker 安装(跨平台/CI 首选,你本机是 Windows 也能用)
# 启动一个 MongoDB 容器(Community 官方镜像),映射 27017
docker run --name mongodb -p 27017:27017 -d mongodb/mongodb-community-server:latest
# 验证容器在跑
docker container ls
# 用 mongosh 连进去(宿主机装了 mongosh 就直接连 localhost:27017;或在容器里执行)
mongosh --port 27017
# 后续第6、8章要做「副本集事务/复制」实验时,用这个单节点副本集起法
docker run --name mongo-rs -p 27017:27017 -d mongodb/mongodb-community-server:latest --replSet rs0MongoDB 5.0+ 的 Docker 镜像要求 CPU 支持 AVX 指令集(近十年 CPU 基本都支持)。
1.3.3 MongoDB Atlas(免安装,云端 5 分钟)
如果只想练 API、不想折腾本地环境:注册 Atlas 云账号 → 创建 M0 Free Tier 集群 → 「Connect → Java」拿到形如 mongodb+srv://user:pass@cluster0.xxxx.mongodb.net/ 的连接串,直接跳到 1.5 用 Java 连。
1.4 mongosh 与 Compass:两个必会工具
装完先记住对照:mongosh ≈ mysql 命令行客户端,Compass ≈ Navicat/DBeaver。
1.4.1 mongosh 三板斧
// 连接:mongosh "mongodb://localhost:27017" 进去后:
show dbs // 看所有库(对照 MySQL SHOW DATABASES)
use yjoffer // 切库/建库(库不存在则首次写入时创建)
db.showCollections() // 看集合(对照 SHOW TABLES)
// 插入一条
db.t_user.insertOne({ _id: 1, username: "tom", age: 20 })
db.t_user.find({ age: { $gte: 18 } }) // 查询(对照 SELECT ... WHERE age>=18)
db.t_user.updateOne({ _id: 1 }, { $set: { age: 21 } })
db.t_user.drop() // 删集合(对照 DROP TABLE)
// 元信息
db.version() // 服务端版本
db.runCommand({ serverStatus: 1 }) // 服务状态(对照 SHOW STATUS)上面「三板斧」里的每一步,用 Java Driver 写出来是完全对应的形状(Filter 对应查询对象、$set 更新文档原样照搬):
MongoClient client = MongoClients.create("mongodb://localhost:27017");
MongoDatabase db = client.getDatabase("yjoffer"); // 对应 use yjoffer
MongoCollection<Document> users = db.getCollection("t_user"); // 对应 db.t_user
// mongosh: db.t_user.insertOne({ _id: 1, username: "tom", age: 20 })
users.insertOne(new Document("_id", 1).append("username", "tom").append("age", 20));
// mongosh: db.t_user.find({ age: { $gte: 18 } })
users.find(Filters.gte("age", 18)).into(new ArrayList<>());
// mongosh: db.t_user.updateOne({ _id: 1 }, { $set: { age: 21 } })
users.updateOne(Filters.eq("_id", 1), new Document("$set", new Document("age", 21)));
// mongosh: db.t_user.drop()
users.drop();
// mongosh: show dbs
db.listCollectionNames(); // 对应 db.showCollections()
for (String name : client.listDatabaseNames()) System.out.println(name);从 mysql 客户端切过来要改的两个手感
- MongoDB 默认不需要先
CREATE:use 新库+ 往新集合insertOne一条,库和集合就都建好了。 - 查询不是写 SQL 字符串,而是调用
find(filterDoc, projectionDoc)方法,参数是 JS 对象——这套对象语法和 Java Driver 里的Filters/Projections完全一一对应,第 2 章会打通。
1.4.2 Compass 图形化
用连接串 mongodb://localhost:27017 连接后,左侧选库/集合,可视地浏览文档、执行查询、看索引与聚合管道预览、看执行计划(第 4 章会用到它的 EXPLAIN 视图)。学习阶段强烈建议开着它——把抽象的文档/索引变成看得见的界面。
1.5 Java 首次连接:跑通再登记信息
引入官方同步驱动(Maven 中央仓库可直接拉取,版本以最新 5.x 为准):
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>5.2.0</version>
</dependency>import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
public class FirstConnect {
public static void main(String[] args) {
// 连接串:本地默认 mongodb://localhost:27017,Atlas 用 mongodb+srv://...
try (MongoClient client = MongoClients.create("mongodb://localhost:27017")) {
MongoDatabase db = client.getDatabase("yjoffer"); // 库不存在会自动延迟创建
// 插入一条,证明连接通了
db.getCollection("t_health").insertOne(new Document("ping", 1));
// 读回来
Document one = db.getCollection("t_health").find().first();
System.out.println("连接成功,读到: " + one.toJson());
// 打印服务端信息,确认版本
System.out.println("当前库: " + db.getName());
}
}
}验证连接是否真的写进去了?回到 mongosh 读一遍(两个工具操作的永远是同一份数据,互为验证工具):
use yjoffer
db.t_health.find() // 应能看到 { ping: 1 } 那条文档
db.t_health.drop() // 体检完成,清理掉跑通后登记第 2~7 章要用的信息:
- 连接串:
mongodb://localhost:27017(本地)或 Atlas 的mongodb+srv://... - 练习库名:
yjoffer - 驱动:
mongodb-driver-sync 5.x,核心入口类MongoClients/MongoClient/MongoDatabase/MongoCollection
线程模型提醒(对照 MySQL 连接池)
MongoClient 本身是线程安全、内含连接池的,一个应用通常只创建一个实例全程复用——别像每次 new Connection 那样每次 new client。这点在 Spring 集成(第 7 章)里由框架自动管理。
1.6 本章小结
- MongoDB 用文档存数据,一个文档自包含一整块业务数据;建模从「拆表」转向「聚合」(第 3 章)。
- 概念对照记死:库/集合/文档/字段 ↔ 库/表/行/列;
_id(ObjectId) ↔ 主键;27017 ↔ 3306;oplog ↔ binlog;WiredTiger ↔ InnoDB。 - 环境搭建三条路:Windows MSI / Docker / Atlas 免费集群;工具两件套 mongosh + Compass。
- Java 侧一个
MongoClient全局复用,getDatabase → getCollection就能读写。
✍️ 课后练习
- 用 1.2 的对照表,把你在 MySQL 里最熟的一张表「口述」成 MongoDB 文档,指出哪些字段可以内嵌成数组/子文档。
- 在 mongosh 里:新建库
yjoffer、集合t_student,插入 3 个字段不完全相同的文档,再find()全部查出来,观察「同一集合不同字段」的表现。 - 用 Docker 起一个带
--replSet rs0的容器(为第 6 章事务做准备),并用 Java 程序连上它写入一条数据。 - (思考题)为什么 MongoDB 能在「无固定 schema」的前提下仍然保证每个文档有唯一主键?
_id起了什么作用?
📖 参考答案
第 1 题:把 MySQL 表「口述」成 MongoDB 文档
拿最常见的「用户三件套」演示,第 1 步列出输入(MySQL 里的一行主表 + 两行从表):
-- t_user: (id=1, username='tom', pwd='***', status=1)
-- t_profile: (user_id=1, city='杭州', bio='Java 工程师')
-- t_address: (user_id=1, addr='西湖区A号'), (user_id=1, addr='滨江区B号')第 2 步问自己:这三张表是不是「总是一起读」?(用户详情页一次全加载)——是 → 合并成一个文档:
// users 集合里的一个文档(输出形状)
{
_id: ObjectId("68f3..."), // ← t_user.id 换成 ObjectId,或直接用业务主键 1
username: "tom", pwd: "***", status: 1,
profile: { city: "杭州", bio: "Java 工程师" }, // 1:1 → 子文档内嵌
addresses: [ "西湖区A号", "滨江区B号" ] // 1:N 少 → 数组内嵌
}第 3 步标出反例:如果某个从表「独立高频写」或「无界增长」(如用户登录日志),就不能内嵌,要拆独立集合存 userId 引用——判据到第 3 章展开。
第 2 题:mongosh 建库建集合 + 异构字段文档
第 1 步:连上本地服务并切库(库不存在不用 CREATE,首次写入自动实体化):
use yjoffer第 2 步:插 3 个字段不完全相同的文档:
db.t_student.insertOne({ name: "amy", age: 20, major: "CS" })
db.t_student.insertOne({ name: "bob", age: 22 }) // 没有 major
db.t_student.insertOne({ name: "coco", major: "数学", extra: { gpa: 3.8 } }) // 没有 age,多了嵌套字段第 3 步:查出来观察「schema 自由」:
db.t_student.find().pretty()
// 输出:三条文档字段各不相同,缺的字段不是 NULL,是「根本没这个键」
// 对照 MySQL:同表所有行必须同列,缺值只能 NULL——这就是「无固定 schema」的第一现场第 4 步(加验):db.t_student.find({ age: { $exists: false } }) 能精确查出 bob——这个手感第 2 章会反复用到。
第 3 题:Docker 起副本集容器 + Java 写入
第 1 步:启动单节点副本集(与 standalone 只差一个参数):
docker run -d --name mongo-rs -p 27017:27017 mongo:8.0 mongod --replSet rs0第 2 步:初始化副本集配置(只做一次,不初始化服务不接受读写):
docker exec mongo-rs mongosh --eval "rs.initiate({_id:'rs0', members:[{_id:0, host:'localhost:27017'}]})"
# 提示符会从 PRIMARY> 出现,说明本节点已当选主节点第 3 步:Java 连接写入并读回(代码与 1.4.1 体检程序一致,连接串不用改):
try (MongoClient client = MongoClients.create("mongodb://localhost:27017/yjoffer?directConnection=true")) {
MongoCollection<Document> coll = client.getDatabase("yjoffer").getCollection("t_rs_test");
coll.insertOne(new Document("from", "java").append("ok", 1));
System.out.println(coll.find().first().toJson());
}
// 预期输出:{"_id": ObjectId("68f3..."), "from": "java", "ok": 1}单机副本集用
directConnection=true避开驱动拓扑探测;第 6 章事务环境就用这个容器,不用再折腾。
第 4 步:mongosh 交叉验证 db.t_rs_test.find() 能看到同一条数据,然后 drop 清理。
第 4 题(思考题)参考思路
要点三条:① 「无 schema」只约束业务字段,_id 是系统级硬性字段——每个文档必须有且集合自带 _id 唯一索引(不可删);② 插入时不写 _id,驱动/服务端自动生成 ObjectId(12 字节 = 4s 时间戳 + 5 随机值 + 3 自增计数器),同机器多进程也不碰撞;③ 本质上它是「主键约束 + 自动生成器」的结合,MySQL 里这两件事靠 AUTO_INCREMENT + PRIMARY KEY 声明完成,MongoDB 把它内置进了类型系统。
下一章我们进入正题:用 Java Driver 完成文档的增删改查,并把 MySQL 的 WHERE / 投影 / 排序 / 分页逐条翻译过来。
