跳到主要内容
约 11 分钟5 张卡

JPA

JPA 是 Java 对象持久化规范,规定实体怎样映射到数据库并参与读写。它通过持久化上下文管理实体,也就是记录哪些对象正在被跟踪、哪些字段发生了变化。

本文从实体身份和状态转换讲起,再解释脏检查、懒加载与事务边界,最后说明关联写入和乐观锁。内容面向 Spring Boot 2.7 与 3.x,分别对应常用的 Hibernate 5.6 与 6.x。规范术语以 Jakarta Persistence 3.1 为参照。

1. 面试核心​

先记住这几条,再读正文

2. 持久化上下文与实体身份​

同一条数据库记录被读取两次时,JPA 需要知道这两次读取得到的是同一个实体。EntityManager 是管理实体的接口,它关联一个持久化上下文。

在同一上下文中,实体类型与主键共同确定持久化身份,同一身份对应唯一的受管对象:

Order first = entityManager.find(Order.class, 101L);
Order second = entityManager.find(Order.class, 101L);

assert first == second;

两次 find 返回同一个对象,所以 first == second 成立。这种身份映射减少重复加载,也让框架能跟踪对象变化和关联关系。它只在当前上下文中生效,上下文关闭后,原对象就会脱管。

EntityManager 及其上下文面向单线程使用,应按一次业务工作管理,避免被多个线程同时访问。

3. 实体状态​

判断字段修改能否写入数据库,要先确认对象处于哪种状态。JPA 实体有以下四种状态:

  • new(新建):对象刚创建,尚未与持久化上下文关联。
  • managed(受管):对象由当前上下文跟踪,字段变化会在 flush 时同步到数据库。
  • detached(脱管):对象有持久化身份,但已离开管理它的上下文,修改只影响这个 Java 对象。
  • removed(待删除):对象已被标记删除,删除操作在同步时执行。

调用实体操作时,受管与脱管状态按以下规则转换:

  • persist 把新实体交给上下文管理,使它进入 managed 状态。
  • find 或实体查询返回 managed 对象,后续修改由上下文跟踪。
  • detach 让指定对象脱管,clear 或关闭上下文让其中的实体脱管。
  • remove 把受管实体标记为 removed,等待执行删除。

4. merge 的状态复制与返回值​

脱管对象的修改需要重新交给上下文跟踪时,可以调用 merge。它返回的是接收这些状态的受管对象:

Order detached = previousOrder; // 来自已关闭的上下文
Order managed = entityManager.merge(detached);

merge 根据持久化身份找到受管目标,再把传入对象的状态复制过去。目标可以是上下文已有的实例,也可以是 JPA 实现新建的受管副本。这里的 JPA 实现指 Hibernate 这类执行规范的框架。

原来的 detached 仍然脱管,所以后续应修改返回的 managed。若传入对象本来就已受管,返回的就是该对象。也就是说,是否新建副本取决于对象状态与当前上下文。

在同一个事务中读取并修改受管实体,flush 时就会同步变化,因此每次 setter 后都无需再调用 save 或 merge。

5. 脏检查与 flush​

脏检查用于找出受管实体中发生变化的字段,flush 则把这些变化同步到数据库。所以下面的代码只修改对象,也能完成更新:

@Transactional
public void rename(long id, String name) {
Customer customer = entityManager.find(Customer.class, id);
customer.rename(name);
}

find 得到的 customer 处于受管状态。事务提交前进行 flush 时,Hibernate 检测到名称变化,就会生成对应的更新 SQL。

Hibernate 可以保存加载时的字段快照,在 flush 时比较新旧值。也可以使用字节码增强,在字段变化时记录修改。两种方式都用于发现变化,具体采用哪一种由实现和配置决定。

flush 完成后,SQL 已同步给数据库,但事务仍可回滚。主键生成策略会影响插入 SQL 的执行时机;查询前是否同步则受 FlushMode 与查询类型影响。后面的 FlushMode 与查询可见性会说明这一点。

6. 事务与上下文边界​

确定对象能被跟踪多久,需要同时看事务和持久化上下文的生命周期。关闭 OSIV、且使用事务范围上下文时,Spring 会在事务结束后关闭上下文,实体随之脱管。OSIV 是把上下文保留到 Web 响应阶段的机制,后文会单独说明。扩展上下文则可以跨越多个事务。

服务方法应在明确的事务内完成读取、校验和修改。事务回滚后,Java 字段没有自动恢复到数据库旧值的保证。所以重试时应开启新事务、重新读取实体,再执行完整业务判断。

把实体长期保存到 HTTP Session、缓存或消息中,会让脱管对象进入其他请求或线程。这时,对象中的版本和关联状态都来自之前的上下文,版本冲突与关联加载就更难判断。

7. 懒加载与上下文​

懒加载把关联查询推迟到真正访问关联数据时。Hibernate 用代理对象或持久化集合保存关联信息,访问尚未初始化的内容时再执行查询:

Order order = repository.findById(id).orElseThrow();
return order.getItems().size();

这里的 size() 会访问订单明细集合。如果集合尚未加载,且 Hibernate Session 已关闭,默认配置下会抛出 LazyInitializationException。Hibernate Session 是 Hibernate 管理持久化上下文的接口,已加载的字段在关闭后仍可访问。

上下文仍可用时,直接序列化实体会让 getter 在生成响应时触发尚未加载的关联查询。双向关联若未配置序列化边界,还会引起递归访问。把所有关联设成 EAGER,又会扩大每次查询固定加载的数据量,并引入次级查询。

服务层可以在事务内按用例加载所需数据,再映射为 DTO。DTO 是只保存响应所需字段的数据对象,这样查询位置和数量就更清楚。

具体抓取方式见 N+1 查询。

8. OSIV 的上下文范围​

OSIV(Open EntityManager in View)让持久化上下文保留到 Web 响应阶段。因此,序列化访问未加载关联时,仍能执行查询,但原业务事务已经结束。

额外查询发生在 Controller 或序列化阶段后,就需要单独考虑数据一致性和错误处理。它们分散在服务方法之外,也更难统计一次业务操作到底查询了多少次。

是否启用 OSIV,要结合应用的响应方式决定。API 服务可以在服务事务内完成查询与 DTO 映射,让响应直接使用已准备的数据。测试还应覆盖真实序列化过程,确认额外查询的位置和数量。

9. 乐观锁与版本校验​

两个事务同时读取同一实体时,后提交的修改会面临覆盖前一次修改的风险。@Version 用版本号识别这种冲突:

@Version
private long version;

更新 SQL 会把读取时的版本加入条件,更新成功后再递增版本。若其他事务已经修改了记录,旧版本条件就无法匹配,Hibernate 会报告乐观锁异常。脏检查负责发现本地变化,版本校验负责发现并发覆盖。

发生冲突后,业务可以提示用户、重新合并修改,或重试。重试需要重新读取实体并执行完整业务判断,单独重发最后一条 update 会沿用过期判断。

10. 关联所有权、级联与抓取​

关联关系涉及怎样写入、怎样加载,以及父子对象何时一起保存或删除。这些行为由不同配置控制,需要分别理解。

双向关系的 owning side 称为拥有方,由它决定数据库中的关系写入。反向一侧用 mappedBy 指向拥有方的字段。只修改反向集合时,外键是否更新仍取决于拥有方状态。所以应用应同时维护两侧对象关系,让内存中的引用与数据库关系一致。

cascade 控制实体操作是否传播到关联对象,比如保存父对象时是否一起保存子对象。fetch 控制关联的加载要求,它们分别管理生命周期操作和读取时机。

JPA 的关联默认加载方式如下:

  • ManyToOne 和 OneToOne 默认 EAGER,要求关联被加载。
  • OneToMany 和 ManyToMany 默认 LAZY,提示按需加载。
  • LAZY 允许实现提前加载,EAGER 也允许用额外的次级查询加载。因此,两者都未规定必须使用 join。

orphanRemoval 适用于 OneToOne 与 OneToMany。受管子实体从关系中移除后,会在 flush 时被删除。它适合只属于父对象的子实体。共享对象应单独定义生命周期,随父对象级联 REMOVE 会影响其他引用它的对象。

11. 批量修改与上下文同步​

JPQL 是面向实体编写查询的语言,它的批量 update/delete 会直接修改数据库。因为批量语句绕过逐个实体的管理过程,已加载对象仍保持旧值,普通实体版本校验也被绕过。

若批量修改依赖版本来防止覆盖,就要在语句或业务逻辑中明确版本条件、版本递增和冲突检查。处理已有受管实体时,应按同步、批量执行和刷新的顺序进行:

  1. 先 flush,把需要保留的内存修改同步到数据库。
  2. 再执行批量 DML,也就是批量更新或删除语句。
  3. 执行后用 clear 清理上下文,或用 refresh 重新读取需要的实体。

提前 clear 会丢弃尚未同步的修改。@Version 冲突则可以到 flush 或 commit 时才暴露,所以应以事务结束结果判断是否成功。出现冲突后,应结束当前事务,在新上下文中重新读取并判断。

12. FlushMode 与查询可见性​

修改受管实体后立即查询,需要知道查询能否看到刚才的修改。FlushMode 控制同步时机,默认的 AUTO 会考虑影响查询结果的待写变化。

在事务内、且持久化上下文已加入事务时,AUTO 要求查询结果反映相关受管实体的更新。实现可以通过 flush,也可以通过其他方式满足要求。

COMMIT 模式要求在提交时 flush,但允许实现提前同步。提交之前,内存修改对查询结果的影响未作规定。若代码需要一个明确的同步点,可以显式调用 flush:

entityManager.setFlushMode(FlushModeType.COMMIT);
customer.rename("new-name");
entityManager.flush();

假设 customer 已受管,上面的 flush 会先把新名称同步到数据库。事务仍需 commit 才算提交,在此之前仍可回滚。

13 · 面试题卡片​

本页题目与参考回答

13.1 JPA 实体为什么修改字段后可以自动更新​

出现公司:阿里巴巴、蔚来

考察重点:说明持久化上下文怎样跟踪受管实体,并区分脏检查、flush 和 commit。能解释脱管、merge 与懒加载之间的关系。

相关内容:持久化上下文、实体状态、脏检查。

参考回答

JPA 会跟踪受管实体的字段变化,在 flush 时通过脏检查生成更新 SQL,所以修改字段后可以自动更新。

通过 find、实体查询或 persist 得到的受管对象由持久化上下文管理。Hibernate 可以比较字段快照,也可以用字节码增强记录修改。flush 只把变化同步到数据库,事务随后仍可回滚。

上下文关闭后,对象脱管,后续字段修改只影响 Java 对象。需要保存这些修改时,可以用 merge 将状态复制到受管实例,并继续使用返回对象。懒加载也依赖可用上下文,因此接口所需关联应在事务内加载,再转换为 DTO。

13.2 merge 返回的对象与传入对象有什么关系?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明 merge 把状态复制到哪个对象,并区分传入对象与返回对象是否受管。

相关内容:merge 的返回值。

参考回答

merge 返回接收状态的受管实例,传入的脱管对象仍然脱管。

框架根据实体身份选择复制目标:当前上下文已有该实体时复用它,否则创建受管副本。因此,返回值是否新建取决于上下文。若传入的本来就是受管对象,返回的仍是该对象。后续修改应作用于返回值,才能继续被上下文跟踪。

13.3 EAGER、LAZY、cascade 和 mappedBy 各自控制什么?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分关联的加载要求、操作传播与关系所有权,说明各项配置怎样影响读写。

相关内容:懒加载、关联边界、OSIV。

参考回答

EAGER、LAZY 控制关联的加载要求,cascade 控制实体操作传播,mappedBy 标出双向关系的拥有方字段。

EAGER 要求关联被加载,实现仍可使用次级查询。LAZY 提示按需加载,也允许实现提前读取。cascade 决定 persist、merge、remove 等操作是否传播到关联对象。关系写入则由拥有方状态决定,应用要同时维护内存中的两侧引用。

OSIV 让响应阶段仍可懒加载,但原业务事务按服务边界结束。因此,响应阶段新增的查询还要单独考虑一致性。

13.4 JPQL 批量更新后,已加载实体和版本号会自动同步吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释批量 DML 为什么绕过受管对象,以及怎样同步上下文并执行版本校验。

相关内容:批量修改、乐观锁。

参考回答

JPQL 批量更新后,已加载实体和版本号都不会自动同步。

批量 DML 直接修改数据库,绕过受管对象的逐项更新和普通版本校验。若需要防止并发覆盖,就要在批量语句或业务逻辑中明确版本条件、递增和冲突检查。

执行前先 flush 需要保留的修改,执行后再 clear 或 refresh。提前 clear 会丢弃尚未同步的字段变化。发生版本冲突后,应结束当前事务,再用新上下文读取并重试。

13.5 AUTO 和 COMMIT 模式下,查询前一定会执行 flush 吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明两种 FlushMode 对查询可见性和同步时机分别有哪些要求。

相关内容:FlushMode 与查询可见性。

参考回答

两种模式都未要求每次查询前一定执行 flush,区别在于查询必须看到哪些变化。

事务内且上下文已加入事务时,AUTO 要求查询考虑影响结果的待写变化。实现可通过 flush,也可通过其他方式满足要求。COMMIT 要求提交时同步,但提交前的查询能否反映内存修改未作规定,框架也可提前 flush。

需要明确同步点时,可以显式调用 flush()。这只把变化写到数据库,事务仍需提交。

14. 参考资料​