实验材料状态:本仓库尚未包含 examples/hibernate-lab/ 工程及原始证据。下文命令和输出来自云端文稿记录,待补交原文件后复核,当前不能据此认定实验已验收;运行命令暂不可在本仓库复跑。

一行不是一个全局 Java 对象

订单服务按数据库主键查询 PurchaseOrder 两次,两次返回值是否满足 ==?只有在说明查询发生在哪个持久化上下文、上下文是否被清空之后,这个问题才有答案。同一张订单在不同请求中也可能同时以两个 Java 对象表示,不能据此推断数据库有两行,或从 Java 引用相同推断另一个事务的可见性。

业务模型沿用第 00 篇的单表订单:id 是数据库标识,order_no 是业务编号,request_key 是幂等键。这里的工作单元仅表示一个 Session 内跟踪的实体集合与变更,不等同于“某次方法调用”“一条物理连接”或者“数据库的一次提交”。本篇没有增加关联实体,更没有用单表测试宣称抓取对象图的成本已经得到验证。

对象身份的作用域是上下文

PurchaseOrder 没有重写 equals()。因此 == 只判断两个 Java 引用是否指向同一实例;数值相等的 id 则判断它们是否映射到同一张订单。独立的 Session 各有自己的持久化上下文,即使在同一个 SessionFactory 中,仍可分别装载代表同一行的不同实例。clear() 解除当前上下文的托管关系,不删除数据库行,也不会把旧对象所有字段抹掉。

冻结源码中,EntityKey 的构造拒绝空标识符;哈希及比较使用实体根类型和标识符的类型契约,而不是调用任意业务对象的 equals()。StatefulPersistenceContext.addEntityHolder() 以 EntityKey 为键存放实体 holder:如果键已存在,则保留已有 holder 并把实体放进去;否则放入新 holder。getEntity(EntityKey) 在没有 holder 或 holder 仍未初始化时返回 null,否则返回已加载实体。这一分支说明“键存在”仍需要检查初始化状态,不能把所有代理行为概括成普通 Map.get()。

入口 SessionImpl.get(Class, Object) 委派给 byId(...).load(...),不是每次显式 new 出对象后无条件发 SQL。持久化上下文只保证本上下文已托管实例的一致引用;不承担第二级缓存的跨 Session 复用,更不修改数据库隔离级别。对应上游 MultiLoadTest.testDuplicatedRequestedIds() 对同一上下文请求重复 id 时用 assertSame 验证实例身份,但它的入口是 multiLoad,不代替本篇的两 Session 与 clear 实验。

从状态角度看,第一次 get 可能经 JDBC 装载实体并把它纳入本上下文;第二次相同标识的 get 可以复用托管实例。另一 Session 的上下文没有这个条目,需要独立读取或从其他缓存机制获取数据,得到的 Java 引用不共享。清空第一个上下文后,原对象变为脱管对象,下一次加载得到新的托管实例。这里的“需要读取”不是保证必定执行一次 JDBC 查询:二级缓存、锁模式和获取策略可能改变实际 SQL;本章没有打开二级缓存,实际日志另列。

单表实验把相同与不同分别断言

实验直接复用 examples/hibernate-lab/ 的 PurchaseOrder 和 PostgreSQL 表。Chapter01Test 用独立写入 Session 创建合成订单并提交,接着打开两个只读观察用的 Session,按同一 id 各自加载;先比较引用,随后对第一个 Session 执行 clear() 再加载。测试代码的核心断言如下,完整依赖、建表、资源关闭与异常回滚由工程提供:

1
2
3
4
5
6
7
8
9
10
11
PurchaseOrder first = firstSession.get(PurchaseOrder.class, orderId);
PurchaseOrder repeated = firstSession.get(PurchaseOrder.class, orderId);
PurchaseOrder independent = secondSession.get(PurchaseOrder.class, orderId);
assertSame(first, repeated);
assertNotSame(first, independent);
assertEquals(first.getId(), independent.getId());
firstSession.clear();
assertFalse(firstSession.contains(first));
PurchaseOrder afterClear = firstSession.get(PurchaseOrder.class, orderId);
assertNotSame(first, afterClear);
assertEquals(orderId, afterClear.getId());

反例不是制造另一个数据库订单:第二个 Session 和清空后的第一个 Session 都得到不同的 Java 引用、相同的数据库 id。因此 first != independent 不能作为“下单重复”的检测条件;比较业务请求幂等键与数据库唯一约束才是另一个边界。只凭 SQL 文本出现次数也不等于驱动 prepare 或网络往返次数,本实验只对 Java 身份与数据库行做断言。

在 PostgreSQL 16.15、OpenJDK 21.0.12.1、Hibernate 7.1.36.Final 下运行 ./mvnw -B -ntp '-Dtest=Chapter00Test,Chapter01Test' test,退出码 0;第 00 篇两项回归和本篇一项断言均通过,无跳过。完整环境、SQL 与绑定参数、退出码在 examples/hibernate-lab/evidence/01/20261002T043030Z/。本篇的打印与 SQL 片段对应同一条订单 id=18:

1
2
3
4
DEBUG: select po1_0.id,po1_0.amount,po1_0.order_no,po1_0.request_key,po1_0.status from purchase_order po1_0 where po1_0.id=?
TRACE: binding parameter (1:BIGINT) <- [18]
OBJECT_IDENTITY id=18 same-session=true second-session=false
OBJECT_IDENTITY after clear same-object=false same-row=true

原始日志中读取该 id 的 SQL 文本出现三次:第一个 Session 首次读取、第二个 Session 首次读取、第一个 Session clear() 后再次读取;第一个 Session 在清空前第二次 get 没有新增对应 SQL 文本。另起 psql 连接按 id=18 查询终态,仅有一行,金额 5.00。这些事实解释本次 Java 引用变化和数据库行数的分离,不能推成 JDBC prepare/网络往返的精确次数,更不能推成生产环境所有隔离级别或启用第二级缓存时的 SQL 保证。

join 行数为什么不能用对象数代替

当一个查询要返回父订单及子行时,即使同一个 Session 里订单对象只有一份,关系结果集也未必只有一行。先不引入 OrderLine 映射,手算以下未来模型的内连接:订单 A 有行 1、行 2,订单 B 有行 3。orders JOIN lines ON ... 产生 (A,1)、(A,2)、(B,3) 三行,结果中的不同订单 id 只有 两个。订单 C 没有行,内连接不出现;换成左连接才可能多出 (C,NULL)。这只是关系运算的手算,不是本章已执行过集合抓取分页实验。持久化上下文维护 Java 对象身份,不会替数据库减少 join 的中间行数或网络传输量。

手算题。 同一个 Session 内对 A 执行两次 get,在 B Session 再执行一次 get;随后 A Session 调用 clear() 并再次查询。若记录了这四个引用,最多有几个不同的 Java 实例、对应几个数据库主键?答案是三个实例、一个主键:A 的前两次同一引用,B 的一次独立,A 清空后再次加载得到第三个实例。本章不从这一答案推算 JDBC 查询次数。

改动练习。 注释掉 firstSession.clear(),保留 assertNotSame(first, afterClear)。预期失败于这一句,因为 first 仍是同一个 Session 托管的已加载实例;再把 clear() 放回去,预期断言通过。这项修改只改变上下文内部身份,并不对数据库行执行删除或更新。若想验证后者,需要另起连接读取或检查 SQL/DML,而不能从 contains() 推断。

参考资料与实验入口

  • Jakarta Persistence 3.2 官方规范:持久化上下文及实体身份的规范约束。
  • Hibernate ORM 7.1 Session API:get、contains、clear 的公开契约;7.1 在线文档可能滚动,不用作冻结补丁的源码快照。
  • 上述 Hibernate 源码与测试均锁定 commit ca7715d7c1afbd46d518752115848bffd9322413;教学测试 examples/hibernate-lab/src/test/java/blog/hibernate/Chapter01Test.java,原始实验记录位于 examples/hibernate-lab/evidence/01/20261002T043030Z/。