JPA、Hibernate 与 Spring Data JPA:实体状态和事务边界
将对象赋值并不等于数据库已提交;同一个查询慢四倍,也可能只是遍历关联时触发了 N+1。本页区分三层:Jakarta Persistence 规定实体、持久化上下文和事务契约;Hibernate ORM 是其中一个 provider;Spring Data JPA 在 JPA 之上提供 repository、派生查询、分页和审计,不是 Hibernate 的别名。事务代理与连接传播回查 Spring 事务,同属持久层但会话模型不同的 SQL Mapper 回查 MyBatis。
U43 新工程固定 Jakarta Persistence 3.2.0、Hibernate ORM 7.2.25.Final、Spring Data JPA 4.0.8、Spring Boot 4.0.9 / Framework 7.0.10、Liberica JDK 21.0.11、Connector/J 9.7.0,以本机 MySQL 8.4.7 单机实跑。依赖由 Boot BOM 管理;U03 的四服务连接证据不代替本票的 JPA 验证。完整可运行输入、实体与数据源配置在 实验入口,本页 SQL 次数与错误码仅描述指定组合,不推成规范保证或生产容量。
实体身份、映射与仓储新旧判断
@Entity 标记实体类型,@Id 给持久化身份;同一持久化上下文中,一个实体类型和主键对应一个受管身份。@Table、@Column、嵌入值、继承策略和关联映射决定对象与表之间的约束;Java equals/hashCode 若在持久化前后随生成 ID 改变,放入 HashSet 后可能无法找到原对象,应选择稳定业务键或谨慎定义身份语义。数据库唯一约束、非空和外键仍是最终数据约束,注解不能替代迁移与数据库核验。
实验 Account 使用应用指定 @Id、非基本类型 @Version Long 和 @CreatedDate;JpaLab 的 @EnableJpaAuditing 与 @EntityListeners 才使审计回调参与。数据库列为 DATETIME(6),应用时间与数据库时区、精度在跨系统审计时仍需校验。部门与员工示例通过 @ManyToOne(fetch=LAZY) 建立关系,仍由数据库外键约束实际行;单侧注解不会自动授权跨租户访问。
Board 与 WorkItem 是双向一对多:子项 @ManyToOne 的 @JoinColumn 管外键,父项 @OneToMany(mappedBy="board", cascade=ALL, orphanRemoval=true) 是反向侧。board.add(item) 和 board.remove(item) 同时维护两端;只把子项加入集合却不设置外键拥有侧,会留下不一致对象关系,并可能违反非空外键。实验持久化父对象级联保存两项,移除一项后 flush 再回滚仍有两项,另一事务移除并提交后剩一项。这里 orphan removal 用于真正由父对象独占生命周期的子项;共享标签或历史凭证不应套用同样删除语义,bulk SQL 也不会逐实体执行这些回调。关联顺序、级联路径和数据库外键须一起验证。
嵌入值是对象的一部分而非独立实体身份,例如 @Embeddable Address 含 city,实体以 @Embedded Address address 和 @AttributeOverride(name="city", column=@Column(name="shipping_city")) 映射到自己的列;对象为 null、列约束、迁移与序列化仍需验证。该片段为映射示意,本票未运行嵌入值或继承策略,不把实体关联实验扩大成全部映射组合。
Spring Data save 默认先看非 primitive @Version 是否为 null,若没有可用版本属性再看 ID;判为新实体调用 persist,否则调用 merge。手工分配 ID 却无可用新实体标记时可能被判为已有实体,按实际业务实现 Persistable.isNew() 等策略并测试。merge 复制状态到返回的受管实例,原脱管对象不会因此变成受管对象;事务结束后返回对象也可能再次脱管。实验在事务内同时断言两者身份与 contains,而不是只看 save 返回值。来源:Spring Data JPA 4.0 实体持久化、SimpleJpaRepository 4.0.8、Jakarta Persistence 3.2 规范。
Source: persist and merge
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}isNew selects the branch; merge returns the EntityManager result.
持久化上下文、脏检查与 flush
新建对象经 persist 后为 managed;find 在上下文存在同一身份时可返回同一受管对象;remove 标记删除;detach、clear 或上下文关闭后对象脱管。对已存在的受管实体修改字段,provider 在适当时点比较并生成更新;脱管对象的字段修改不会自动写回。本票在独立事务中先提交实体,再在另一事务 find 并修改而不调用 save,观察新事务读到新值且版本递增;再修改脱管对象,数据库不变,经 save 的 merge 才生效。
flush 把待执行变更同步到数据库事务,不等于物理 commit。实验 persist → flush 后用独立连接读取不到未提交记录,回滚后新事务仍查不到;这是该 MySQL 隔离和实验条件的观察,不代表所有数据库隔离配置一样。JPA AUTO 的查询契约是使可能影响查询结果的待处理变化可见,provider 可用 flush 或其他方式实现,不能把“每次查询必发 SQL”当规范。COMMIT 也允许 provider 在提交前更早 flush;没有活动事务或上下文未加入事务时不得向数据库 flush。Hibernate AUTO 在提交前及 JPQL/HQL 相关 query space 等条件下可能 flush;native SQL 的触发取决于 API 和同步空间。定位时记录事务边界、flush mode、实际 SQL 和结果,而不是按 save 调用次数推测。来源:JPA 3.2 §3、Hibernate 7.2 flush、固定版本 DefaultFlushEventListener。
Spring 的 JpaTransactionManager 按 EntityManagerFactory 绑定事务资源;注入的共享 EntityManager 是门面,不是跨线程共享的可变持久化上下文。固定版本源码中,doGetTransaction/doBegin 查找或建立 holder 并开始事务,doCommit/doRollback 调用实体事务完成,doCleanupAfterCompletion 解除绑定和释放资源。实验使用单个 factory、同一专用 DataSource 和 TransactionTemplate;一个事务中受管对象、连接和上下文的存活范围要一起确认,跨线程或异步任务不继承当前事务。@Transactional(readOnly=true) 是事务与 provider 优化提示,不是通用的写入授权或禁止校验;自调用、事务传播和异常回滚规则见 Spring 事务章节。资料:Framework 7.0.10 JpaTransactionManager、Spring Data 事务。
抓取、N+1 与分页不能只数实体
EAGER 是要求取得关联而非保证单条 JOIN;JPA 的 LAZY 是提示。实验先查 3 个员工、各自访问 3 个不同部门,Hibernate 统计的 prepare 数为 1+3=4;仓储的 @EntityGraph(attributePaths="department") 在该输入下为 1。计数是该 provider 的语句准备次数,不等于网络往返次数、生产 QPS 或所有映射的保证。定位应先固定输入、事务范围、访问路径、一级/二级缓存和日志级别,再对比 SQL、结果行数、执行计划与内存。
join fetch、EntityGraph 和按批加载都可能减少查询,但集合 JOIN 会乘行并增加对象去重与内存;给集合 fetch join 同时设置分页,JPA 规范对结果未定义。Hibernate 在某些场景会内存分页;可考虑开启 hibernate.query.fail_on_pagination_over_collection_fetch 把不安全路径暴露为错误,同时检查具体版本生成的 SQL。大列表可先按稳定唯一排序键查询 ID 页,再按这些 ID 批量抓关联并恢复原顺序;需要考虑两次查询之间的数据变化、IN 长度和一致性要求,不是一种无代价的替换。来源:JPA 3.2 §3.11、Hibernate 7.2 fetching 与分页设置。
事务、乐观版本与悲观锁
@Version 为受管实体更新提供乐观并发检查;两个独立上下文都读取相同版本 v 后,A 提交并使版本前进,B 的旧版本更新应失败,不能吞掉异常然后声称两次修改都成功。实验中 A 成功、B 的提交抛含 OptimisticLockException 的回滚异常,最终保留 A;随后新事务重读 A 的结果并完成一次明确的新修改。重试必须重新读取并重新判断业务不变量;扣款、外部副作用仍需独立幂等协议。
悲观锁的持有期由数据库事务决定。实验以一个 JPA EntityManager 对账户取 PESSIMISTIC_WRITE,第二条独立 JDBC 连接设置本会话的 innodb_lock_wait_timeout=1 后更新同一行,观察 MySQL 错误码 1205;第二连接回滚,释放第一事务后重试成功。它不证明第二次 JPA 锁请求的异常分类,也不证明死锁或跨主机故障。JPA 规范 区分事务级回滚的 PessimisticLockException 与只导致语句级回滚的 LockTimeoutException;Spring 异常传播和具体数据库事务状态还须分别验证。锁顺序、事务时长、等待上限、重试预算和超时后重新读取都要进入设计。
查询、批量操作与缓存一致性
仓储 AccountRepository 以 findByNameStartingWith 派生查询并返回 Page;实验用稳定 id 排序、每页1条,断言总数2。Page 的 count 查询可能被优化跳过;如果自定义 native SQL 分页,复杂查询常需显式 countQuery,且数据 SQL 与 count 必须使用相同租户与权限条件,避免总数侧信道。Slice 可只回答是否有下一页;keyset 分页需要稳定唯一排序键,任意跳页与并发变化需另外设计。JPQL 操作实体和属性,native SQL 操作数据库对象,不能把二者的方言、参数绑定或执行计划视为相同。
批量 JPQL 更新直接作用数据库,不同步现有持久化上下文,且不自动执行实体级乐观版本检查;实验中更新后旧 managed 对象仍旧,clear() 后重新查询看到新值,随后事务回滚恢复。clear() 会丢弃尚未 flush 的受管修改;要按语义选择先 flush、更新、再 clear 或使用新上下文。@Modifying 的 clearAutomatically 也是这个权衡,不会自动为业务补版本条件。若要求乐观并发,需要在 WHERE 中比较 version、按语义递增并检查受影响行数。见 JPA 3.2 §3.11 和 Spring Data 修改查询。
实验把 hibernate.jdbc.batch_size=3,以应用分配 ID 的5个实体分段 flush/clear:第一次整事务回滚为0行,第二次提交为5行。该断言证明结果与事务边界,不证明驱动合并为一次网络请求或任何吞吐提升;标识生成策略、驱动、SQL 形状、版本和参数上限均会改变有效批处理。超大事务会占内存和锁、增加回滚成本;可比较可恢复分片(不再全局原子)与暂存表校验后切换。
一级缓存是持久化上下文身份映射,不是数据库事务隔离;clear 或独立上下文只绕过一级缓存;验证数据库值还要按实际配置绕过或驱逐二级缓存,或使用独立 JDBC 查询,并说明事务隔离。二级缓存是 provider/缓存实现的可选能力,本实验未启用,也没有验证跨实例失效。外部 JDBC/其他服务修改表后不能假定 provider 缓存自动同步;需要明确缓存范围、失效协议与最终一致窗口,余额和库存裁决仍应使用数据库约束。批量写入也可能绕过实体回调、级联和自动审计;需按操作路径验证,而非假设所有写入都执行相同生命周期逻辑。
Repository 目录与证据边界
Spring Data JPA 的派生查询、@Query、projections、stored procedures、Specifications、Query by Example、事务与锁、审计、扩展 repository、事件和 AOT 分属不同能力;本票实跑派生查询、分页、接口 projection、单条件 Specification、单字段 QBE、@EntityGraph、save、@Modifying 和 @CreatedDate。例如 AccountRepository 的 projection 返回 ID/名称,Specification 用 Criteria 构造条件,QBE 忽略 ID/version/审计字段后按名称匹配;这些只是当前数据的用法,不代表它们自动附带租户授权或支持任意动态查询。声明式查询方法默认不自动带事务;继承 CRUD 的事务默认值与其不同,须看实际服务层边界。其余能力按 官方 4.0 参考目录 查证,不因 starter 在类路径上就声称均已验证。JPA 测试切片 在 H2 内存库运行两个测试,检查 repository 映射、测试间回滚及非持久层组件被排除;它不验证 MySQL 方言或整个应用。过程、复杂 Specifications/QBE、批量删除回调、审计当前用户、多结果集与跨库分布式事务未在本实验运行。Security 对象授权示例回查 资源授权与租户边界,它不代替数据库授权验证。
闭卷验收
本页 14 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。