预见猿份
主题
首页面试题模拟面试SQL练习在线工具关于我们老苗一对一私教学员评价
实战项目
零基础学习路线项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通达梦数据库从入门到实战MongoDB从入门到实战PostgreSQL从入门到实战TiDB从入门到实战Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价
blog我的会员
  • MongoDB从入门到实战|Java 程序员的文档数据库课程

    • 课程介绍
    • 第1章 认识MongoDB与环境搭建
    • 第2章 文档CRUD与Java Driver实战
    • 第3章 数据建模与Schema设计
    • 第4章 索引与性能优化
    • 第5章 聚合管道
    • 第6章 事务、并发与一致性
    • 第7章 Spring Data MongoDB实战
    • 第8章 副本集、分片与运维安全







----- 到底线了 -----

第2章 文档 CRUD 与 Java Driver 实战 ​

🎯 本章学习目标 ​

  1. 掌握 MongoClient → MongoDatabase → MongoCollection<Document> 三层入口,理解客户端线程模型。
  2. 会 insertOne / insertMany,理解 _id/ObjectId 的生成规则与批量插入 ordered 语义。
  3. 把 MySQL WHERE 的常用条件(=、范围、IN、LIKE、IS NULL、AND/OR)逐条翻译成 Filters。
  4. 会用 Projections 控制返回字段(对照 SELECT 列)、Sorts + skip + limit 实现排序分页(对照 ORDER BY / LIMIT OFFSET)。
  5. 掌握更新操作符 $set / $inc / $push / $unset / $pull(对照 UPDATE 列赋值,外加数组操作这个 MySQL 没有的能力)。
  6. 掌握删除语义与「清空集合」的正确姿势;会用 bulkWrite 把增删改混合成一次请求提交;初步接触 POJO 映射(完整版在第 7 章)。

本章统一使用官方同步驱动 mongodb-driver-sync 5.x。所有 mongosh 写法与 Java 写法成对给出——它们是同一套查询对象语法的两种外壳。


2.1 三层入口与准备数据 ​

java
MongoClient client = MongoClients.create("mongodb://localhost:27017");   // 全局一个,别每次 new
MongoDatabase db = client.getDatabase("yjoffer");
MongoCollection<Document> users = db.getCollection("t_user");
1
2
3

本章示例数据——往 t_user 塞几个字段并不完全一致的文档(感受一下无固定 schema):

javascript
// mongosh:准备同一批种子数据
use yjoffer
db.t_user.insertMany([
  { username: "tom",  age: 25, city: "武汉", tags: ["java", "mysql"],          status: 1 },
  { username: "amy",  age: 30, city: "北京", tags: ["python"],                 status: 1 },
  { username: "bo",   age: 22, city: "武汉",                                   status: 0 },  // 没有 tags 字段
  { username: "cathy",age: 35, city: "上海", tags: ["java", "spring", "mongodb"], status: 1 }
])
1
2
3
4
5
6
7
8
java
// Java Driver:同一批数据,Document.append 链式拼装
List<Document> seeds = List.of(
    new Document("username", "tom").append("age", 25).append("city", "武汉")
        .append("tags", List.of("java", "mysql")).append("status", 1),
    new Document("username", "amy").append("age", 30).append("city", "北京")
        .append("tags", List.of("python")).append("status", 1),
    new Document("username", "bo").append("age", 22).append("city", "武汉")
        .append("status", 0),                                    // 没有 tags 字段
    new Document("username", "cathy").append("age", 35).append("city", "上海")
        .append("tags", List.of("java", "spring", "mongodb")).append("status", 1)
);
users.insertMany(seeds);
1
2
3
4
5
6
7
8
9
10
11
12

2.2 插入:insertOne / insertMany ​

关注点MySQLMongoDB
单行/多行INSERT INTO ... VALUES 一条/多条insertOne / insertMany
主键AUTO_INCREMENT 回填不传 _id 则自动生成 ObjectId;结果里 insertedId 可拿
批量失败行为事务回滚或逐条报错insertMany 默认 ordered:遇错即停,之前的已插入;unordered 尽力全插
重复主键Duplicate entry 报错同名 Unique index violation:_id 重复同样报 DuplicateKeyException
java
// 单条:拿回生成的 _id
InsertOneResult r = users.insertOne(new Document("username", "dave").append("age", 28));
ObjectId newId = (ObjectId) r.getInsertedId().asObjectId().getValue();

// 自定义 _id(合法:任何唯一、可哈希的值都行,数字/字符串均可,但分布式场景推荐 ObjectId 或雪花ID字符串)
users.insertOne(new Document("_id", 1001).append("username", "admin"));

// 批量 + 无序(对照「批量导入尽量多成功」场景)
users.insertMany(moreDocs, new InsertManyOptions().ordered(false));
1
2
3
4
5
6
7
8
9
javascript
// mongosh 对照
db.t_user.insertOne({ username: "dave", age: 28 });        // 返回 { acknowledged: true, insertedId: ObjectId(...) }
db.t_user.insertOne({ _id: 1001, username: "admin" });
db.t_user.insertMany(moreDocs, { ordered: false });
1
2
3
4

ObjectId 是什么

12 字节:4 字节时间戳 + 5 字节随机值 + 3 字节自增计数器。自带生成时间、无需协调、分布式友好——这解释了 MongoDB 为什么不需要 MySQL 式的 AUTO_INCREMENT。前 4 字节可反解创建时间(new ObjectId(id).getTimestamp()),排查「这条数据什么时候进来的」很好用。

2.3 查询条件:WHERE → Filters 逐条对照 ​

这是日常开发最高频的部分,先上对照表再讲细节:

MySQL WHEREJava Driver (Filters)mongosh
age = 25Filters.eq("age", 25){age: 25}
age &lt;> 25Filters.ne("age", 25){age: {$ne: 25}}
age >= 18 AND age < 30Filters.gte("age",18) + Filters.lt("age",30)(组合见下){$gte/$lt}
city IN ('武汉','北京')Filters.in("city", List.of("武汉","北京")){$in: [...]}
city NOT IN (...)Filters.nin(...){$nin: [...]}
username LIKE 'to%'Filters.regex("username", "^to"){username: /^to/}
status IS NULL(或字段缺失)Filters.exists("status", false). 或 Filters.eq("status", null){status: {$exists: false}}
A AND BFilters.and(f1, f2) 或直接两个条件链式组合{$and: [...]}(多数可省略)
A OR BFilters.or(f1, f2){$or: [...]}
布尔列Filters.eq("deleted", false){deleted: false}
数组包含 'java'Filters.eq("tags", "java")(对数组字段等值即「包含」){tags: "java"}
数组全含 a,bFilters.all("tags", List.of("a","b")){$all: [...]}
数组长度=3Filters.size("tags", 3){$size: 3}
按下标匹配Filters.eq("tags.0", "java"){"tags.0": "java"}

组合查询完整示例:

javascript
// mongosh 对照:同一个组合条件,只是把 Filters 换成字面量对象
db.t_user.find({
  status: 1,
  $or: [{ city: "武汉" }, { city: "北京" }],
  age: { $gte: 18 }
}).sort({ age: -1 })
1
2
3
4
5
6
java
// 对照 SQL: WHERE status=1 AND (city='武汉' OR city='北京') AND age>=18 ORDER BY age DESC
Bson filter = Filters.and(
        Filters.eq("status", 1),
        Filters.or(Filters.eq("city", "武汉"), Filters.eq("city", "北京")),
        Filters.gte("age", 18)
);

for (Document u : users.find(filter).sort(Sorts.descending("age"))) {
    System.out.println(u.getString("username") + " / " + u.get("age"));
}
1
2
3
4
5
6
7
8
9
10

类型是严格匹配的!MySQL 会做的隐式转换这里不做

Filters.eq("age", 25) 查不到存成字符串 "25" 的文档;eq("id", 1) 也查不到 _id 为 NumberLong(1) 或字符串 "1" 的文档(Int32/Int64/Double 在 MongoDB 比较时按数值处理,但数字与字符串之间绝不互转)。MySQL 里 WHERE varchar_col = 123 能匹配 '123' 的隐式转换习惯,在这里只会让你查不到数据还以为是缓存问题。

2.4 投影与排序分页:SELECT 列 / ORDER BY / LIMIT ​

2.4.1 Projections 对照 SELECT 列 ​

javascript
// mongosh 对照(find 的第二个参数就是投影文档)
db.t_user.find({}, { username: 1, age: 1 })                       // include
db.t_user.find({}, { content: 0 })                                // exclude
db.t_user.find({}, { _id: 0, username: 1, tags: { $slice: 2 } })  // 排除 _id + 数组截取
// 数组内条件取一个元素:投影里的 $elemMatch
db.t_user.find({}, { orders: { $elemMatch: { status: 1 } } })
1
2
3
4
5
6
java
// 对照 SQL: SELECT username, age FROM t_user   (默认 _id 总会返回,除非显式排除)
Bson proj = Projections.include("username", "age");
users.find().projection(proj);                        // mongosh: db.t_user.find({}, {username:1, age:1})

// 只排除某大字段:对照 SELECT 除了 content 都要
Projections.exclude("content");

// 计算列、改名、数组截取等进阶:
Projections.fields(
    Projections.excludeId(),
    Projections.rename("username", "name"),
    Projections.slice("tags", 2),                    // 数组只回前2个
    Projections.elemMatch("orders", Filters.eq("orders.status", 1)) // 数组内条件过滤(对照 JSON 数组里"挑一个元素")
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

2.4.2 排序 + 分页:对照 ORDER BY ... LIMIT n OFFSET m ​

javascript
// mongosh 对照
db.t_user.find().sort({ age: -1, _id: 1 }).skip(20).limit(10)
1
2
java
// SQL: SELECT * FROM t_user ORDER BY age DESC, id ASC LIMIT 10 OFFSET 20
users.find()
     .sort(Sorts.orderBy(Sorts.descending("age"), Sorts.ascending("_id")))
     .skip(20)
     .limit(10)
     .into(new ArrayList<>());
1
2
3
4
5
6

分页三件套心智与 MySQL 完全一致;但性能教训也完全一致:skip(1000000) 依然要扫过前一百万条再丢弃,深分页同样慢。对策也同款——书签法:Filters.gt("_id", lastId).limit(10),用游标代替 OFFSET(第 4 章索引章节还会强调)。

2.4.3 count 与 exists ​

javascript
// mongosh 对照
db.t_user.countDocuments({ status: 1 })
db.t_user.estimatedDocumentCount()
1
2
3
java
long total = users.countDocuments(Filters.eq("status", 1));   // 对照 SELECT COUNT(*) WHERE status=1
// 估算级:集合大、只要大概数时用(走元数据,快但不保证精确)
long estimate = db.getCollection("t_user").estimatedDocumentCount();
1
2
3

2.5 更新:UPDATE → updateOne/updateMany + 操作符 ​

MySQL 的 UPDATE 只能「给列赋新值」;MongoDB 更新必须通过操作符表达(没有操作符的裸文档 = 整档替换,是经典事故源):

javascript
// mongosh 对照:更新文档里的 $ 操作符与 Java 手写的 Document 完全同形
db.t_user.updateOne({ username: "tom" }, { $set: { age: 26, city: "深圳" } });
db.t_user.updateOne({ username: "tom" }, { $inc: { age: 1 } });
db.t_user.updateOne({ username: "tom" }, { $push: { tags: "juc" } });
db.t_user.updateOne({ username: "tom" }, { $addToSet: { tags: "jvm" } });
db.t_user.updateOne({ username: "tom" }, { $pull: { tags: "mysql" } });
db.t_user.updateOne({ username: "tom" }, { $unset: { city: "" } });
db.t_user.replaceOne({ username: "bo" }, { _id: boId, username: "bo" });
db.t_user.updateMany({ status: 0 }, { $set: { status: 2 } });
db.t_user.updateOne(
  { username: "eve" },
  { $setOnInsert: { age: 18 }, $set: { status: 1 } },
  { upsert: true }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java
// 对照 SQL: UPDATE t_user SET age=26, city='深圳' WHERE username='tom'
users.updateOne(
    Filters.eq("username", "tom"),
    new Document("$set", new Document("age", 26).append("city", "深圳"))
);

// 对照 UPDATE ... SET stock = stock - 1 WHERE ...(原子扣减,MySQL 同款防超卖语义)
users.updateOne(Filters.eq("username", "tom"), new Document("$inc", new Document("age", 1)));

// 数组操作——MySQL JSON 列做不到这么顺手的部分:
users.updateOne(Filters.eq("username", "tom"), new Document("$push", new Document("tags", "juc")));    // 追加元素
users.updateOne(Filters.eq("username", "tom"), new Document("$addToSet", new Document("tags", "jvm"))); // 去重追加
users.updateOne(Filters.eq("username", "tom"), new Document("$pull", new Document("tags", "mysql")));   // 按值删除元素

// 删除字段(对照 SET col = NULL 但更彻底:字段真的没了)
users.updateOne(Filters.eq("username", "tom"), new Document("$unset", new Document("city", "")));

// 全档替换(危险:漏字段=丢数据,生产慎用;等价 DELETE+INSERT)
users.replaceOne(Filters.eq("username", "bo"), new Document("_id", boId).append("username", "bo"));

// 批量更新(对照 UPDATE 无 WHERE 主键时命中多行)
users.updateMany(Filters.eq("status", 0), new Document("$set", new Document("status", 2)));

// upsert:有则更、无则插 —— 对照 INSERT ... ON DUPLICATE KEY UPDATE
users.updateOne(
    Filters.eq("username", "eve"),
    new Document("$setOnInsert", new Document("age", 18)).append("$set", new Document("status", 1)),
    new UpdateOptions().upsert(true)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

返回对象 UpdateResult 提供 getMatchedCount / getModifiedCount / getUpsertedId——对应 MySQL 里「匹配行数 vs 实际变更行数」的经典区分(值相同则 matched=1、modified=0,两边一致)。

事故预警:裸文档更新

users.replaceOne(eq("username","tom"), new Document("age", 26)) 或 updateOne(filter, new Document("age",26))(不带操作符)会把整个文档替换得只剩 age。写更新操作时,第一个 $ 键没进去,就是数据删除的开始。Code Review 时值得单独盯这条。

2.6 删除:DELETE / TRUNCATE / DROP 对照 ​

javascript
// mongosh 对照
db.t_user.deleteOne({ username: "eve" });
db.t_user.deleteMany({ age: { $lt: 18 } });
db.t_user.deleteMany({});     // 清空(TRUNCATE 语义)
db.t_user.drop();             // 删集合(DROP TABLE 语义)
1
2
3
4
5
java
users.deleteOne(Filters.eq("username", "eve"));     // 对照 DELETE ... WHERE username='eve' LIMIT 1
users.deleteMany(Filters.lt("age", 18));            // 对照 DELETE ... WHERE age<18

// 清空集合数据但保留索引与集合定义(对照 TRUNCATE TABLE)
users.deleteMany(new Document());

// 删整个集合(对照 DROP TABLE;连索引一起没了)
users.drop();
1
2
3
4
5
6
7
8

注意两点:

  1. MongoDB 的 DELETE 默认逐条写 oplog、不锁全表,删大量数据期间集合仍可读可写(对照 MySQL 大事务 DELETE 的锁与 binlog 压力,体验更好,但空间不会立即归还磁盘,需要 compact 或等回收)。
  2. deleteOne 命中多条时删哪条不保证顺序(除非配合排序再 limit——精确做法:先 find 排序取 _id 再按 _id 删)。

2.7 批量操作 bulkWrite:一次请求混合增删改 ​

对照 JDBC 的 addBatch()/executeBatch():把多个操作攒成一次网络往返提交。注意 MongoDB 的批量是「混合操作」而非「同操作多参数」。

javascript
// mongosh
db.t_user.bulkWrite([
  { insertOne: { document: { username: "frank", age: 27, status: 1 } } },
  { updateOne: { filter: { username: "amy" }, update: { $set: { city: "杭州" } } } },
  { deleteOne: { filter: { username: "dave" } } }
], { ordered: true })
1
2
3
4
5
6
java
// Java Driver:每个动作包成 XxxModel,一次性提交
users.bulkWrite(List.of(
        new InsertOneModel<>(new Document("username", "frank").append("age", 27).append("status", 1)),
        new UpdateOneModel<>(Filters.eq("username", "amy"), new Document("$set", new Document("city", "杭州"))),
        new DeleteOneModel<>(Filters.eq("username", "dave"))
), new BulkWriteOptions().ordered(true));
1
2
3
4
5
6

ordered(true)(默认)按顺序执行、遇错即停;ordered(false) 尽可能多执行——与 insertMany 的语义一致。生产上做「初始化一批数据」这类场景,bulkWrite 比逐条调用快一个量级(省往返)。

2.8 POJO 映射初探:别再手搬 Document ​

裸 Document 取值 doc.get("age") 没类型安全,真实项目要映射成实体。两条路:

  1. 第 7 章主角 Spring Data MongoDB:注解 @Document/@Id 全自动,推荐;
  2. 纯 Driver 项目的轻量方案:POJO Codec(社区编解码器,注解极少、零 Spring 依赖):
java
public class User {
    @Id                       // org.bson.codecs.pojo.annotations.BsonId:映射到 _id
    private ObjectId id;
    private String username;
    private Integer age;
    private List<String> tags;
    // 无参构造 + getter/setter 必需(POJO Codec 基于反射实例化)
}

// 给集合挂上 POJO 编解码 Registry(PojoCodecProvider 内置于驱动 bson 包,无额外依赖)
CodecRegistry pojoRegistry = CodecRegistries.fromRegistries(
        db.getCodecRegistry(),
        CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build()));

MongoCollection<User> userColl = db.getCollection("t_user", User.class)
        .withCodecRegistry(pojoRegistry);

User u = userColl.find(Filters.eq("username", "tom")).first();   // 直接拿到强类型对象
userColl.insertOne(newUser);                                     // 实体直接入库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

POJO Codec 能解决 80% 的映射需求;但它对复杂类型、懒加载、与 Spring 生态的整合都不如专业框架完善——企业项目直接用第 7 章的 Spring Data MongoDB,注解更丰富、还自动处理类型转换。

2.9 Java 程序员十大注意点(本章精华) ​

#注意点对应 MySQL 习惯
1MongoClient 全局单例、线程安全别按请求 new 连接
2数值/字符串类型严格匹配,不隐式转换WHERE varchar=123 那套没了
3更新必须带 $ 操作符,裸文档=替换没有「UPDATE t SET」的语法糖
4对数组字段 eq 即「包含」JSON 数组查询的直觉要重建
5字段缺失 ≠ null?$exists 才精确NULL 一值两用 vs 「没这个键」
6skip 深分页照样慢,用书签法与 MySQL LIMIT 大偏移同病
7_id 默认 ObjectId,自带时间可反解自增 id 的「可比时间」特性丢了
8库/集合首次写入自动创建CREATE DATABASE/TABLE 仪式感没了(生产要靠规范约束!)
9批量插入默认 ordered,遇错即停与 JDBC batch 行为不同,需显式设 unordered
10删除大集合不立即释放磁盘InnoDB DELETE 空间回收心智要更新

2.10 本章小结 + 练习 ​

  1. 三层入口 + 一个全局 client;find(filter, projection).sort().skip().limit() 就是「SELECT…WHERE…ORDER BY…LIMIT」。
  2. Filters 对照表覆盖 95% 的 WHERE 翻译需求;数组与「字段缺失」查询是 MongoDB 独有的两块新 territory。
  3. 更新操作符 $set/$inc/$push/$pull/$unset + upsert,等价并超出 MySQL 的 UPDATE/ON DUPLICATE 能力。
  4. 删除的 deleteOne/deleteMany/drop 三件套与「空间不即还」的运维意识;混合批量用 bulkWrite 省往返。
  5. 本章所有 mongosh/Java 成对示例其实是同一套查询对象语法的两个外壳——以后遇到任何 shell 写法,都可以直接「翻外壳」成 Java,反之亦然。

✍️ 课后练习 ​

  1. 把下面这条 SQL 翻译成 Java Driver 调用(含投影、排序、分页): SELECT username, age FROM t_user WHERE status = 1 AND age BETWEEN 20 AND 30 AND tags LIKE '%java%' ORDER BY age DESC LIMIT 10 OFFSET 0;
  2. 用 $push + $addToSet 给文档 tags 数组分别追加元素并观察重复行为;再用 $pull 删掉其中一个。
  3. 制造一次「裸文档替换事故」:replaceOne 只传一个 age 字段,find 验证数据丢了什么,再想办法补回来(练习 upsert 与 $set 的正确姿势)。
  4. (思考题)为什么 Filters.eq("_id", "665f1a2b...")(字符串)查不到数据?正确写法是什么?这体现了 MongoDB 的什么类型规则?

📖 参考答案 ​

第 1 题:SQL → Java Driver

第 1 步,准备输入数据(t_user 5 条,注意 tags 是数组字段——MySQL 的 LIKE '%java%' 在这里对应「数组包含」语义,不是正则):

javascript
// mongosh 播种
 db.t_user.insertMany([
  { username: "amy",  age: 25, status: 1, tags: ["java", "mysql"] },
  { username: "bob",  age: 35, status: 1, tags: ["java"] },
  { username: "coco", age: 22, status: 0, tags: ["java", "redis"] },   // status=0 被过滤
  { username: "dave", age: 19, status: 1, tags: ["go"] },              // age 不在区间
  { username: "eve",  age: 28, status: 1, tags: ["python", "java"] }
])
1
2
3
4
5
6
7
8

第 2 步,逐句翻译 WHERE:status=1 → eq、BETWEEN 20 AND 30 → gte+lte、LIKE→数组直接 eq(第 2 章注意点 #4);投影/排序/分页:include("username","age")、descending("age")、skip(0).limit(10)。

第 3 步,完整 Java 调用:

java
FindIterable<Document> rs = users.find(new Document("$and", List.of(
                Filters.eq("status", 1),
                Filters.gte("age", 20),
                Filters.lte("age", 30),
                Filters.eq("tags", "java")))                    // 数组包含,等价 LIKE '%java%' 的意图
        .projection(Projections.include("username", "age"))
        .sort(Sorts.descending("age"))
        .skip(0).limit(10);
rs.forEach((Block<? super Document>) d -> System.out.println(d.toJson()));
1
2
3
4
5
6
7
8
9

第 4 步,mongosh 等价验证 + 预期输出(amy 25 在前、eve 28 在后?——age 降序,eve28 > amy25,所以 eve 第一行):

javascript
db.t_user.find({ $and: [{status:1},{age:{$gte:20}},{age:{$lte:30}},{tags:"java"}] },
               { username: 1, age: 1, _id: 0 })
  .sort({ age: -1 }).skip(0).limit(10)
// { "username": "eve",  "age": 28 }    ← 28 在 25 前面,降序
// { "username": "amy",  "age": 25 }
1
2
3
4
5

若 tags 真是字符串列(不是数组),才需要 Filters.regex("tags", "java"),且无锚定的正则不走索引(第 4 章)。

第 2 题:$push / $addToSet / $pull 重复行为

第 1 步,播种一个干净的 tags:

javascript
db.t_user.updateOne({ username: "amy" }, { $set: { tags: ["java"] } })
1

第 2 步,观察两种追加的差异(重复值!):

javascript
db.t_user.updateOne({ username: "amy" }, { $push: { tags: "java" } })
db.t_user.findOne({ username: "amy" }, { tags: 1 })
// { tags: [ "java", "java" ] }        ← $push 不去重,重复照收

db.t_user.updateOne({ username: "amy" }, { $addToSet: { tags: "java" } })
db.t_user.findOne({ username: "amy" }, { tags: 1 })
// { tags: [ "java", "java" ] }        ← 已存在,$addToSet 静默不添(matchedCount=1, modifiedCount=0)

db.t_user.updateOne({ username: "amy" }, { $addToSet: { tags: "mysql" } })
// { tags: [ "java", "java", "mysql" ] }  ← 不存在才添
1
2
3
4
5
6
7
8
9
10

第 3 步,$pull 删掉一个(注意:pull 按值删,同值全部移除,不是删一个):

javascript
db.t_user.updateOne({ username: "amy" }, { $pull: { tags: "java" } })
// { tags: [ "mysql" ] }   ← 两个 "java" 一次全删!想「删一个」要改用 $pull FILTER 或重写数组
1
2

Java 侧同构:updateOne(Filters.eq("username","amy"), new Document("$addToSet", new Document("tags","mysql")))。

第 3 题:裸文档替换事故与补救

第 1 步,备份认知制造事故(拿 coco 开刀,她有 4 个字段):

javascript
db.t_user.findOne({ username: "coco" })
// { _id: ..., username: "coco", age: 22, status: 0, tags: ["java","redis"] }

db.t_user.replaceOne({ username: "coco" }, new Document("age", 99))   // ⚠️ 裸文档 = 整档替换
1
2
3
4

第 2 步,find 验证丢了什么:

javascript
db.t_user.findOne({ username: "coco" })
// { _id: ..., age: 99 }     ← username/status/tags 全没了!只剩 _id(不可变)+ 新文档的 age
1
2

第 3 步,补救 + 正确姿势:

javascript
// 补救:用 $set 把缺的键填回去(事故现场能恢复的前提是你记得原值/有备份)
db.t_user.updateOne({ username: null, age: 99 },
  { $set: { username: "coco", status: 0, tags: ["java", "redis"] }, $unset: { age: "" } })
// 注意定位条件也变了(username 被替换没了),这就是事故的代价

// 正确姿势:局部更新永远用 $set;确实要整档替换才用 replaceOne,且带上完整文档
1
2
3
4
5
6

附加练习:replaceOne(filter, doc, new ReplaceOptions().upsert(true)) 体验「命中替换、未命中插入」——对照 INSERT ... ON DUPLICATE KEY UPDATE。

第 4 题(思考题)参考思路

t_user 里 _id 存的是 ObjectId 类型,而 "665f1a2b..." 是 string——MongoDB 的等值比较类型不同即不匹配,没有 MySQL 那种隐式转换(第 2 章注意点 #2)。正确写法:Filters.eq("_id", new ObjectId("665f1a2b..."))(mongosh 里是 ObjectId("665f1a2b..."))。这条规则推广到一切类型:age: 25(int32)查不到存成 25.0(double)或 "25"(string)的文档——这也是为什么 3.5 的 $jsonSchema 和 7.2 的 DECIMAL128 都要强调类型声明。

下一章进入文档数据库的灵魂:数据建模——内嵌还是引用?这是 MySQL 程序员最需要「重装思维」的一章。

← 第1章 认识MongoDB与环境搭建第3章 数据建模与Schema设计 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节