第23章 JPA / Hibernate 异常

第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 不存在"。


相关推荐
用户8181870627461 小时前
第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试
后端
雪隐1 小时前
个人电脑玩AI-15让5060 Ti给你打工——MiniMax H3 本地部署实录:一个自带录音棚的视频模型,和它的 NVFP4 瘦身奇遇
前端·人工智能·后端
玖石书3 小时前
ASP.NET Core 迁移至 Spring系列:类库框架篇
java·后端·asp.net
PieroPC3 小时前
Windows 驱动备份与恢复工具 CMD bat
后端
Csvn3 小时前
📊 SQL 入门 Day 12: 窗口函数基础
后端·sql
玖石书3 小时前
ASP.NET Core 迁移至 Spring系列:编译生态篇
后端·spring·asp.net
Java技术小馆3 小时前
AI 如何理解对话
后端
小满zs3 小时前
Go语言第五章(数组和切片)
后端·go
AskHarries4 小时前
RSS 有必要做吗
后端