深入 Hibernate 03:托管、脱管和删除怎样影响订单
实验材料状态:本仓库尚未包含
examples/hibernate-lab/工程及原始证据。下文命令和输出来自云端文稿记录,待补交原文件后复核,当前不能据此认定实验已验收;运行命令暂不可在本仓库复跑。
修改了对象,为什么订单金额没有变化
一个 PurchaseOrder 从数据库加载后,在业务方法里被 detach,随后金额字段从 5.00 改成 9.99。Java 对象显示 9.99,再次从同一数据库行加载却仍是 5.00。原因不是写入失败,而是这份 Java 对象已脱离持久化上下文,后续字段变化没有自动成为本次工作单元的更新。反过来,对托管对象调用 remove,即使发出 DELETE,另一连接在事务提交前仍可能读到旧行。订单不变量要求区分对象字段、上下文状态与数据库终态,不能从其中一层推断另一层。
本篇沿用第 00 篇的单表订单与 PostgreSQL;第 01 篇已证明两个 Session 对同一行可以持有不同 Java 对象。本篇只增加一个受控金额修改方法和一项集成测试,不为状态变化重建一套工程。
状态属于哪一层
Jakarta Persistence 讨论新建、托管、脱管和删除等实体生命周期的可见行为;Hibernate 内部的 Status 是会话内部记账枚举,包含 MANAGED、DELETED、GONE、LOADING 等值,并不等于规范暴露给应用的状态列表。该枚举没有 DETACHED 成员;StatefulPersistenceContext.clear() 在清空时将 holder 状态改为内部的 EntityHolderState.DETACHED,并解除代理、集合与会话的关联。这两种内部状态对象也不能混为一个枚举。
| 操作与观察 | 原对象/当前 Session | 新连接的订单行 |
|---|---|---|
| new 一个订单 | 尚未托管,contains=false |
0 |
persist 后显式 flush,事务未提交 |
本 Session contains=true,已有 id |
0 |
提交后用另一个 Session get |
加载的对象处于新上下文的托管范围 | 1 |
detach 后改变原对象金额 |
原对象 contains=false,字段已变成 9.99 |
仍有旧值 5.00 |
新加载的托管对象先改成 10.00,再 refresh |
从数据库重取,金额回到 5.00 |
1 |
clear、重新加载后调用 remove 与 flush |
删除动作已发出 | 本章 READ COMMITTED 新连接仍读到 1 |
删除事务 commit 返回成功 |
旧引用不再代表库中的行 | 新连接读到 0 |
这个表不是“对所有隔离级别、所有抓取策略都如此”的保证。PostgreSQL 本次显式新连接查询使用 READ COMMITTED;没有连接并发更新,refresh 的读回值由单测试按真实 SQL 与对象字段共同断言。
源码如何把操作分派成不同状态
固定 ca7715d7c1afbd46d518752115848bffd9322413 的 SessionImpl.detach() 检查会话开放后调用 evict;SessionImpl.clear() 清理当前持久化上下文与动作队列。局部 detach 与全局 clear 因此不能用同一组副作用描述:前者让指定对象离开当前上下文,后者移除当前上下文跟踪的所有对象和待处理动作;二者都不是 SQL DELETE。
删除走另一路径。SessionImpl.remove() 触发 DeleteEvent;DefaultDeleteEventListener.deleteEntity() 保存删除状态,将 EntityEntry 标为 Status.DELETED,再为持久化实体加入删除动作。flush 执行动作队列;数据库收到 DELETE 不代表事务立即提交。这条链包含两处关键分支:detach 指向解除关联而非删除,remove 对托管实体进入删除路径;最终数据库后果由提交或回滚决定。上游 ContainsTest.testLifecycle() 断言对象在一个上下文中托管,在另一个上下文中原引用不再由它管理;它没有检查本篇 PostgreSQL 上删除事务的可见性,后者由教学测试单独验证。
refresh 的实验限定为有对应数据库行的托管对象。它从库中重取当前值以覆盖本次未提交的字段修改,不是“把所有 Java 对象恢复到数据库提交前的快照”:已经脱管的另一份引用仍保有 9.99。remove 之后若事务失败,不能继续把曾经删除过的会话对象当作可靠恢复依据;应回滚、关闭旧会话,并新开连接按业务键核对终态。
单表实验与反例
examples/hibernate-lab/src/test/java/blog/hibernate/Chapter03Test.java 先创建合成订单,再提交。第二个 Session 依次执行 detach、改变脱管对象字段、重新 get、对托管对象执行 refresh、clear、重新加载并 remove;每个会话独立,测试在失败时回滚并关闭资源。运行 ./mvnw -B -ntp '-Dtest=Chapter00Test,Chapter01Test,Chapter02Test,Chapter03Test' test,本次四篇共五项测试成功、零失败、零跳过。原始 SQL、绑定、退出码和工具链记录在 examples/hibernate-lab/evidence/03/20261002T044225Z/:
1 | |
该次运行包含合成订单 state-3d3f515c-8957-4dd0-b3d4-678f13286f7f 的 INSERT 与 DELETE 日志,另起 psql 连接按订单号查询终态为 0 行。日志不能说明执行了几次 prepare、网络传输了几趟;对象字段变化也不代表 UPDATE 已执行。本实验的反例是改动脱管对象金额与删除尚未提交时的新连接读数;读者可从字段与行数之间的差距定位责任层。
手算题。 若 remove 后 flush 已发出 DELETE,却把事务的 commit 改成 rollback,原 Java 对象和另一新连接各能证明什么?原对象可能仍带着既有 id 和字段,但不能证明数据库行已删除;新连接在本实验 READ COMMITTED 隔离下读到提交前的行,回滚后仍能读到。不能把“执行过 DELETE”推成“事务成功取消订单”。
改动练习。 在测试中删除 session.detach(detached),保留对 detached.changeAmount(9.99) 与后续新 get 的断言。此时再次 get 返回同一实例,“两个引用不同”的断言失败;金额还是内存中的 9.99,而非从数据库重取的 5.00。恢复 detach 后复测,把对象状态变化与另一个连接的读取结果分开记录。
参考资料与代码入口
- Jakarta Persistence 3.2 规范:实体生命周期与持久化上下文(规范层)。
- Hibernate ORM 7.1 Session API:
detach、clear、refresh、remove的接口;在线系列文档不是补丁源码快照。 - 上述固定 SHA 的实现和上游测试为源码证据;教学实验代码
examples/hibernate-lab/src/test/java/blog/hibernate/Chapter03Test.java,原始输出examples/hibernate-lab/evidence/03/20261002T044225Z/。两类测试不能互相代替。

