大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
语言驱动
这一课你将能做到
- 说清楚连接串(Connection String)各部分的含义,配好连接池与超时参数
- 用 Java 官方驱动和 Spring Data MongoDB 完成增删改查,理解 ODM 与 ORM 的异同
- 知道 Node.js 侧(mongodb / Mongoose)的对应写法,能对照选型
为什么要学
mongosh 里敲得再熟,最终代码是跑在应用里的。驱动层有自己的一整套坑:连接池打满、ObjectId 序列化成奇怪的对象、Spring Data 的 @Document 映射和手写 BSON 对不上、驱动版本和服务器版本不兼容。本站后端是 Spring Boot,这一课以 Java 为主线,Node 做对照。
人话:mongosh 是「手动挡试车场」,驱动是「自动挡量产车」——同一条命令,多了类型映射、连接池、重试这些量产级考量。
先建立图像
连接串解剖:
mongodb://codeforge_app:App!Pass2026@m1:27017,m2:27017,m3:27017/codeforge
│ │ │ │──────────────│ │
协议 用户:密码 @ 种子节点列表 数据库
(驱动自动发现全集) (默认库+authSource)
常用参数(?后):
replicaSet=rs0 复制集名(自动发现拓扑)
authSource=admin 认证库
maxPoolSize=50 连接池上限
w=majority&journal=true 写关注
readPreference=secondaryPreferred
connectTimeoutMS=3000 建连超时
socketTimeoutMS=10000 读写超时
驱动分层:
你的代码
│
Spring Data MongoDB(@Document / MongoTemplate / Repository) ← ODM 层
│
MongoDB Java Driver(MongoClient / Document / Bson) ← 官方驱动
│
BSON 编解码 + 连接池 + 拓扑发现 + 重试
│
MongoDB Server
跟着做
步骤 1:Java 官方驱动直连
Maven 依赖:
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>5.2.1</version>
</dependency>
import com.mongodb.client.*;
import org.bson.Document;
import static com.mongodb.client.model.Filters.*;
public class DriverDemo {
public static void main(String[] args) {
String uri = "mongodb://localhost:27017/codeforge?maxPoolSize=20";
try (MongoClient client = MongoClients.create(uri)) {
MongoDatabase db = client.getDatabase("codeforge");
MongoCollection<Document> subs = db.getCollection("submissions");
// 插入
Document doc = new Document("userId", 1024L)
.append("lessonSlug", "mdbrm-drivers")
.append("status", "accepted")
.append("score", 96);
subs.insertOne(doc);
// 查询:Filters 对应 mongosh 的查询运算符
for (Document d : subs.find(and(eq("userId", 1024L), eq("status", "accepted")))
.sort(new Document("submittedAt", -1)).limit(10)) {
System.out.println(d.toJson());
}
// 更新:Updates 对应 $set/$inc
subs.updateOne(eq("_id", doc.getObjectId("_id")),
com.mongodb.client.model.Updates.combine(
com.mongodb.client.model.Updates.set("status", "reviewed"),
com.mongodb.client.model.Updates.inc("reviewCount", 1)));
}
}
}
注意 eq("userId", 1024L) 显式用 Long——如果数据库里存的是 int64,传 int 也能匹配(数值跨子类型相等),但保持类型一致是纪律。
步骤 2:Spring Data MongoDB —— ODM 方式
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
application.yml:
spring:
data:
mongodb:
uri: mongodb://localhost:27017/codeforge
实体映射:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.Instant;
@Document("submissions") // 指定集合名,不写默认类名小写
public class Submission {
@Id
private String id; // String 类型自动与 ObjectId 互转
private Long userId;
private String lessonSlug;
private String status;
private Integer score;
private Instant submittedAt;
// getters/setters 省略
}
Repository(声明式,方法名生成查询):
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
public interface SubmissionRepository extends MongoRepository<Submission, String> {
List<Submission> findByUserIdAndStatusOrderBySubmittedAtDesc(Long userId, String status);
List<Submission> findByScoreGreaterThanEqual(Integer score);
long countByLessonSlugAndStatus(String lessonSlug, String status);
}
步骤 3:MongoTemplate —— 复杂查询的出路
方法名表达不了的(聚合、$elemMatch、部分更新)用 MongoTemplate:
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.where;
// 1) 条件更新:等价于 updateOne + $set/$inc
Query q = new Query(where("userId").is(1024L).and("lessonSlug").is("mdbrm-drivers"));
Update u = new Update().set("status", "accepted").inc("attempts", 1);
mongoTemplate.updateFirst(q, u, Submission.class);
// 2) 聚合管道:每节课平均分
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(where("status").is("accepted")),
Aggregation.group("lessonSlug").avg("score").as("avgScore"),
Aggregation.sort(Sort.Direction.DESC, "avgScore")
);
var result = mongoTemplate.aggregate(agg, "submissions", Document.class);
步骤 4:Node.js 对照
import { MongoClient } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017/codeforge", { maxPoolSize: 20 });
await client.connect();
const subs = client.db("codeforge").collection("submissions");
await subs.insertOne({ userId: 1024, lessonSlug: "mdbrm-drivers", status: "accepted" });
const list = await subs.find({ userId: 1024 }).sort({ submittedAt: -1 }).limit(10).toArray();
await client.close();
Mongoose(Node 的 ODM)多了 schema 定义、校验、中间件,相当于 Java 侧的 Spring Data 层。
步骤 5:ObjectId 序列化坑位演练
直接返回 Spring Data 实体时,@Id String 已是纯字符串,天然安全。但手动用 Document 时:
Document d = subs.find().first();
String bad = d.toJson(); // {"_id": {"$oid": "66f1..."}} —— 扩展 JSON 格式
String id = d.getObjectId("_id").toHexString(); // "66f1..." —— 给前端用这个
前端回传查询时,Repository 的 String id 参数 Spring Data 自动转回 ObjectId;手写 MongoTemplate 查询要自己 new ObjectId(hex)。
讲清楚要点
- 驱动内置连接池:
MongoClient是重量级对象,全应用单例复用,千万别每个请求 new 一个。池大小默认 100,按并发量调maxPoolSize。 - 拓扑自动发现:连接串给一个或几个种子节点即可,驱动通过 hello 命令发现复制集全部成员,主切换后自动重连新主。
- 官方驱动 vs Spring Data:驱动层操作
Document(灵活、贴近 shell);Spring Data 把文档映射成 POJO(类型安全、有 Repository 抽象)。两者混用是常态——简单 CRUD 用 Repository,复杂更新/聚合用 MongoTemplate。 - ODM vs ORM:概念同构(对象 ↔ 存储结构映射),但文档模型没有 JOIN,所以 ODM 的「关联」是内嵌文档(直接映射成嵌套对象)或
@DBRef(引用,基本不要用,应用层二次查询更透明)。 - 方法名派生查询的边界:
findByUserIdAndStatus...很爽,但超过两三个条件就可读性崩坏,换@Query注解或 MongoTemplate。 - 驱动/服务器版本兼容:Java 驱动 5.x 兼容 MongoDB 4.0–7.x 服务端;升级前查官方兼容矩阵,别盲升。
- 超时三兄弟:
connectTimeoutMS(建连)、socketTimeoutMS(单次读写)、serverSelectionTimeoutMS(找不到可用节点,默认 30 秒太长,Web 应用建议调小到 5 秒内,快速失败交给上层重试)。
你可能会懵的地方
- MongoTemplate 和 Repository 冲突吗? 不冲突,Repository 底层就是 MongoTemplate。一个项目里两者共存是标准做法。
@DBRef为什么被嫌弃? 它会在查询主文档后由驱动自动或手动发起额外查询拉引用文档,N+1 问题隐形且不可控。明确地在应用层分两步查,性能行为反而可控。- 事务怎么用?
Spring Data 配
MongoTransactionManager(需复制集环境),然后@Transactional照常生效。但记住:好的内嵌模型让大部分事务不必要。 - 连接池打满什么症状?
请求卡在「等连接」上,超时后抛
MongoWaitQueueFullException。排查:慢查询占用连接过久、池太小、或者代码里没复用 MongoClient 建了多个池。 - Java 的 Instant/LocalDateTime 存进去是什么? BSON Date(UTC 毫秒)。驱动自动转换,但历史数据若是字符串(坏味道),查询时对不上——回 BSON 与类型那一课。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
每个请求 MongoClients.create() | 连接风暴,几分钟打挂 | 单例 Bean,try-with-resources 只用于脚本 |
@DBRef 关联一切 | N+1 隐形爆炸 | 内嵌或应用层二次查询 |
实体 _id 用 ObjectId 类型直接暴露给前端 JSON | 序列化成 {timestamp, date} 怪对象 | @Id String,框架自动互转 |
| serverSelection 默认 30s 卡住整个线程池 | 雪崩放大 | 调小到 3–5s + 熔断重试 |
| 在 Repository 写 30 个派生方法 | 维护地狱 | 复杂查询统一 MongoTemplate |
卡点急救
MongoTimeoutException: No server chosen:网络不通/复制集名不匹配/认证失败。先用 mongosh 同串连一遍排除账号问题,再查replicaSet参数是否和实际一致。- 查出来日期差了 8 小时:BSON Date 是 UTC,Jackson 默认按 UTC 序列化。配
spring.jackson.time-zone: GMT+8或前端处理。 - 写入成功但 find 不到:大概率类型不一致(Long vs Integer vs String),日志里打印实际参数类型,对照库里
$type结果。
面试常问
- MongoDB 驱动和 JDBC 的区别? 形态类似(连接池 + 协议),但驱动多了拓扑发现(自动感知复制集主备切换)、游标流式拉取、BSON 编解码;没有 SQL 解析层,查询以文档/过滤器 API 表达。
- Spring Data MongoDB 和 JPA/Hibernate 的使用体验差在哪? 注解和 Repository 风格相似,但没有懒加载代理、没有 JOIN fetch、没有一级缓存;「关联」靠内嵌文档天然解决,因此更不易踩 N+1(除非用 @DBRef)。事务需要复制集且使用面更窄。
- 线上 MongoDB 连接池怎么配?
起点:maxPoolSize = 预期并发线程数(如 Web 容器 200 线程配 50–100 足够,因为单次操作毫秒级);配合 socketTimeout 防僵死连接,监控
waitQueueSize指标,排队说明池小或查询慢。
自测三问
- 为什么
MongoClient必须单例?连接串里哪些参数控制池和超时? - Repository 派生方法和 MongoTemplate 各自适合什么查询?
- 前端传回
_id字符串查不到数据,可能是什么原因?
小结一句话
驱动层三件套:连接池单例、POJO↔BSON 映射、Repository+MongoTemplate 双轨——shell 里的每条命令在代码里都有对应,只是多了类型与资源的边界。
延伸
- MongoDB Java Driver 文档
- Spring Data MongoDB 参考文档
- 下一课:生态集成——Change Streams、Kafka、Elasticsearch,让 MongoDB 接入数据流宇宙。