第23章 JPA / Hibernate 异常
23.1 LazyInitializationException:最经典的 JPA 陷阱
java
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
}
// Service 层
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
} // 事务在这里结束,Session 关闭
// Controller 层(事务已经结束)
Order order = orderService.getOrder(id);
order.getItems().size(); // LazyInitializationException: could not initialize proxy - no Session
根本原因: 懒加载(FetchType.LAZY)的关联对象在首次查询时并没有真正加载数据 ,只是拿到一个代理对象;真正访问该字段(触发 getItems())时才会向数据库发起查询------但这个"延迟查询"依赖 Hibernate 的 Session(一级缓存及数据库连接的载体)仍然处于打开状态。一旦事务方法执行结束(@Transactional 注解的方法返回),Session 会被关闭,此时如果在事务边界之外才第一次访问懒加载字段,就会因为"Session 已关闭,无法发起新查询"而报错。
解决方案(按适用场景排序):
java
// 方案1:在事务方法内部就把需要用到的懒加载数据"提前触发"加载完
@Transactional
public Order getOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
order.getItems().size(); // 在 Session 还开着的时候主动触发加载
return order;
}
// 方案2:查询时直接用 JOIN FETCH 一次性把关联数据加载进来,避免懒加载代理
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
// 方案3(不推荐,权衡取舍):把 FetchType 改为 EAGER,但会导致每次查询 Order 都强制连带查询 items,
// 即使很多场景根本不需要 items 数据,造成不必要的查询开销,只在关联数据"几乎总是需要用到"时才考虑
架构层面的正确做法 :DTO 转换(把 Entity 转换成返回给前端的 DTO)应该在事务方法内部完成,而不是把 Entity 对象直接传到 Controller 层再做懒加载字段访问------这也是分层架构里"Service 层负责数据组装完整,Controller 层只负责结果转发"这一原则的具体体现。
23.2 OptimisticLockException(JPA 内建乐观锁)
java
@Entity
public class Product {
@Version
private Integer version; // JPA 标准的乐观锁字段
}
// 事务A和事务B同时读到 version=1 的记录,都尝试更新
// 事务A先提交成功,version 变为2
// 事务B提交时,发现自己期望更新的 version=1 已经不存在,抛出 OptimisticLockException
和第22章提到的 MyBatis-Plus 乐观锁不同,JPA 标准的 @Version 机制是真的会抛出异常 (javax.persistence.OptimisticLockException 或其 Spring 包装后的 ObjectOptimisticLockingFailureException),而不是像 MyBatis-Plus 那样只是返回 0 行受影响------这是两个框架在同一设计问题(乐观锁冲突该如何反馈)上做出的不同选择,使用时需要注意区分处理方式。
23.3 EntityNotFoundException vs Optional 的选型
java
// Spring Data JPA findById 返回 Optional,需要显式处理"不存在"这种情况
Order order = orderRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND));
// getReferenceById(早期叫 getOne)返回的是一个懒加载代理,不会立即查询数据库,
// 如果这个 id 实际不存在,会在真正访问其字段时才抛出 EntityNotFoundException,而不是在调用当下抛出
Order proxy = orderRepository.getReferenceById(id); // 此时不会报错,即使 id 不存在
proxy.getName(); // 这里才会因为触发懒加载查询发现记录不存在,抛出 EntityNotFoundException
排查陷阱 :getReferenceById()(代理占位)和 findById()(立即查询)在"id 不存在"这件事上的报错时机完全不同,如果代码里误用了前者却期望"调用时立即知道记录是否存在",会导致异常在一个完全出乎意料的、离真正问题很远的地方才被抛出,让排查者困惑"明明是这一行代码报的错,为什么问题却是 id 不存在"。