2.3 持久化:Java帝国反击战
从对象图与关系表手工同步走到显式身份、查询和事务,比较 EJB 与轻量级 O/R Mapping 的责任边界。
学习目标
- 能把 Java 对象的身份、关系访问、SQL 形状和数据库事务边界逐一对齐
- 能比较 EJB 容器式持久化与轻量级 O/R Mapping 在生命周期、查询和故障恢复上的责任
- 能用正常、边界和单一故障样本定位 N+1、断电半提交、重复写入和事务外懒加载
2.3 持久化:Java帝国反击战
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.3 持久化:Java帝国反击战。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
内存里的对象会随着进程退出而消失,数据库里的行却需要身份、关系和提交顺序。持久化层的工作不是把每个字段机械地抄一遍,而是维护一个可解释的映射:对象身份对应主键,关联对应外键或连接表,查询对应明确的 SQL,修改对应一个事务。ORM 可以减少样板代码,但不能替你决定查询计划、锁、提交或断电恢复。
三个会让持久化失真的陷阱
六个目录节点到持久化证据
断电的威胁
↡进程、机器或数据库连接在多步写入中突然停止,迫使系统证明已提交内容和未完成内容如何区分。把“调用 save”变成了真正的故障问题。必须知道日志是否持久、事务是否提交、重启后如何恢复,以及客户端重试会不会重复写入。对象已经修改不等于数据库已经安全保存。
数据库联合酋长国
↡对象模型与关系模型共同协作的持久化边界:对象有生命周期,表有约束、索引、事务和并发语义。提醒我们两种模型不能互相伪装。对象导航方便表达业务关系,关系数据库擅长集合查询和约束;映射层要明确谁负责身份、关联、级联、删除和事务,而不是把一方的默认行为当成另一方的保证。
表面风光的EJB
↡通过容器托管组件、事务、依赖和资源的企业 Java 持久化路径;它的便利来自隐式生命周期,也因此需要显式观察。的价值和风险在同一处:容器可以统一资源和事务,却可能让调用者看不见连接、会话、代理和提交时机。诊断时记录容器事务属性、组件边界、异常传播和数据库实际提交,不以注解存在证明操作成功。
轻量级O/R Mapping框架
↡用映射元数据、会话和查询 API 减少对象与关系表之间样板代码的工具层,不自动消除 SQL、事务和性能成本。把主键、字段、关联和查询写成可测试的映射。它可以让持久化更轻,但懒加载、批量大小、flush、缓存、锁和事务传播仍然是工程决策。每个方便的对象调用都要能追到 SQL 和边界。
帝国的反击
↡在对象模型与关系数据库之间建立可观察、可恢复、可演进的映射合同,用工具减少重复而不隐藏责任。不是框架替代数据库,而是把身份、查询、事务和恢复组合成一条稳定路径。轻量 ORM 反击过度重量的容器,同时保留 SQL 和数据库约束的可见性;选择标准应是故障和查询证据,而不是 API 长短。
2.3 持久化:Java帝国反击战
标题里的“反击”可以落到一个具体合同:对象变化只有在身份可追踪、关系可查询、SQL 可解释且事务已提交后才算持久化。2.3 持久化:Java帝国反击战要求把对象便利性还原成数据库事实,并在断电、懒加载和重复重试时证明恢复路径。
最小持久化合同
record SaveAttempt(
String entityId,
int expectedVersion,
int sqlStatements,
boolean transactionActive,
boolean committed,
boolean idempotentRetry
) {}
static boolean isDurable(SaveAttempt attempt) {
return !attempt.entityId().isBlank()
&& attempt.transactionActive()
&& attempt.committed()
&& attempt.sqlStatements() > 0;
}合同把对象身份、版本、SQL 形状、事务状态和重试策略放在一起。committed 只说明数据库事务边界内的提交,不说明邮件、缓存或外部服务已经完成;跨边界动作还要有幂等键、事件或补偿合同。
一个可观察的批量读取可以写成:
SELECT o.id, o.version, r.id AS relation_id, r.status
FROM orders AS o
LEFT JOIN order_rows AS r ON r.order_id = o.id
WHERE o.id = ANY (:order_ids);这条 SQL 的重点不是具体语法,而是把 N 个对象的关系访问变成一个有边界的集合查询。验收时保存参数、返回行数、索引计划、事务时间和对象组装结果。
四步复核持久化链
1. 固定对象身份和映射
为对象写下主键、版本、字段、关联和删除规则。正常样本从一个对象开始,边界样本重复保存或改变版本,检查是否会覆盖、插入重复行或拒绝。
Lab
持久化边界实验
只改变一个访问或故障条件,观察 SQL 数量、事务状态和重试结果。
20 个订单由一条集合查询加载关系
orders=20 → SQL=1 → rows mapped → transaction committed
判定
accept:查询数与对象图一致
当前样本:批量读取;保存实体 ID、SQL 数量、事务状态、重试键和数据库版本。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 一个事务、身份有效、关系批量读取 | 对象图与关系行一致,SQL 数量可解释 | 主键、版本、SQL、提交记录 |
| 边界 | N 个关系、事务外懒加载或重复保存 | 明确批量、拒绝或版本冲突,不隐式放大 | 查询数、连接、会话、影响行数 |
| 单一故障 | 写入中断、提交后重试或容器异常 | 不重复、不留半成品,恢复动作可追踪 | 日志、事务状态、幂等键、重放 |
故障诊断:从对象偏差追到数据库事实
- 身份层:检查对象 ID、数据库主键、版本和代码路径;同一个对象地址不能作为重启后的身份。
- 查询层:统计 SQL 次数、参数、行数和事务时间;1+N 不是“数据库慢”的结论,而是要先定位懒加载边界。
- 事务层:确认开始、flush、提交、回滚、异常传播和连接归属;事务结束后的代理访问必须有明确策略。
- 恢复层:模拟断电、重复请求和网络重试,比较数据库日志、最终行状态和副作用;只有幂等或补偿合同成立时才能自动重试。
不要用对象图打印结果替代 SQL 和提交证据,也不要用 ORM API 调用返回替代数据库确认。持久化通过必须同时证明身份、查询、事务和恢复四条线。
最小持久化证据包与反例
证据包包含实体主键与版本、映射配置、会话/容器边界、SQL 与参数摘要、查询数量、返回行数、锁/隔离选择、flush/commit/rollback 日志、重试键和重启后的数据库状态。敏感数据可以脱敏,但不能删除定位重复或丢失更新所需的身份。
反例一是读取一页订单后逐个懒加载关系,产生 1+N 查询;反例二是订单行已写入,断电发生在流水提交前,客户端重试又生成第二个订单。保留两条轨迹,修复批量查询或幂等事务后从干净数据库重放。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 断电的威胁
多步写入在进程或机器停止时需要由日志、事务和恢复策略区分已提交与未完成。
- 数据库联合酋长国
对象生命周期与关系表的身份、约束、查询和事务语义共同组成的映射边界。
- 表面风光的EJB
容器托管组件、事务和资源的路径,便利背后仍需观察隐式生命周期。
- 轻量级O/R Mapping框架
用映射元数据、会话和查询 API 减少样板代码的工具层,不消除 SQL 与事务成本。
- 帝国的反击
让对象便利性与可观察、可恢复的数据库合同重新对齐的工程路径。
练习
练习
问题 1: 一页有 20 个订单,日志出现 1 条订单查询和 20 条关系查询。如何确认这是 N+1,修复时要保留哪些不变量?
问题 2: 订单插入成功后进程断电,客户端重试又发来同一个请求。怎样避免两个订单?
问题 3: EJB 或 ORM 方法返回成功,但事务外读取关联关系抛出懒加载异常。你先查哪三个边界?
本页小结
2.3 持久化:Java帝国反击战的关键不是让 ORM 隐藏数据库,而是让对象身份、关系访问、SQL、事务和恢复彼此可验证。完成标准是能识别 N+1、区分 EJB 容器边界与轻量映射边界,并在断电和重试后证明没有重复写入或半提交状态。