1. 概述

在使用 Hibernate 的懒加载(Lazy Loading)时,经常会遇到 LazyInitializationException,提示“no session”——也就是会话已关闭,无法加载延迟数据。

本文将深入探讨这个问题的成因,并介绍一个“捷径”解决方案:enable_lazy_load_no_trans。我们将基于 Spring Boot 构建示例,帮助你理解何时能用、何时该避坑。

⚠️ 提前剧透:这个配置虽然简单粗暴,但不推荐在生产环境使用


2. 懒加载的问题根源

懒加载的核心目标是节省资源:在加载主实体时,并不立即加载其关联对象,而是等到真正访问时才去数据库取数据。Hibernate 通过 代理(Proxy)和集合包装类 实现这一机制。

整个过程分两步:

  1. 加载主对象(如 User
  2. 访问其懒加载属性(如 User.getDocs())时触发代理初始化

✅ 关键点:第二步必须有一个打开的 Hibernate Session

问题就出在:如果访问懒加载属性时,事务已经结束,Session 被关闭,就会抛出 LazyInitializationException

正确做法

理想情况下,我们应该在事务内完成所有需要的数据读取,比如使用 @Transactional 注解确保懒加载属性在事务中被初始化。

现实困境

但在实际开发中,尤其是分层架构(如 Service → Controller → View),我们可能在 Controller 层才访问实体的关联数据,而 Service 层的事务早已提交。

这时候,Hibernate 提供了一个“兜底”方案:enable_lazy_load_no_trans

⚙️ enable_lazy_load_no_trans 是什么?

开启后,每次访问懒加载属性时,Hibernate 会自动创建一个临时 Session,并开启一个独立事务来加载数据。这样就避免了 LazyInitializationException

听起来很香?但别急,后面有“副作用”。


3. 懒加载实战示例

我们通过一个典型的一对多关系来演示不同场景下的行为。

3.1 实体与服务层准备

假设我们有两个实体:UserDocument,一个用户有多个文档。

@Entity
public class User {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "userId")
    @Fetch(FetchMode.SUBSELECT)
    private List<Document> docs = new ArrayList<>();

    // getter & setter
}
@Entity
public class Document {

    @Id
    private Long id;

    private String title;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User userId;

    // getter & setter
}

服务层提供两个方法对比:

@Service
public class ServiceLayer {

    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true)
    public long countAllDocsTransactional() {
        return countAllDocs();
    }

    public long countAllDocsNonTransactional() {
        return countAllDocs();
    }

    private long countAllDocs() {
        return userRepository.findAll()
            .stream()
            .map(User::getDocs)
            .mapToLong(Collection::size)
            .sum();
    }
}

我们使用 SQLStatementCountValidator 来统计 SQL 执行次数,评估性能。


3.2 事务内懒加载(推荐做法)

调用带 @Transactional 的方法:

@Test
public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() {
    SQLStatementCountValidator.reset();

    long docsCount = serviceLayer.countAllDocsTransactional();

    assertEquals(15, docsCount); // 假设总文档数为15
    SQLStatementCountValidator.assertSelectCount(2);
}

✅ 输出结果:2 次查询

  • 第1次:查所有用户
  • 第2次:通过 SUBSELECT 一次性查出所有用户的文档

这是理想情况,高效且安全。


3.3 事务外懒加载(典型报错场景)

调用非事务方法:

@Test(expected = LazyInitializationException.class)
public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() {
    serviceLayer.countAllDocsNonTransactional();
}

❌ 结果:直接抛出 LazyInitializationException

原因:userRepository.findAll() 返回的 User 对象中,docs 是个代理。当在事务外调用 getDocs() 时,Session 已关闭,无法初始化代理。


3.4 开启 enable_lazy_load_no_trans(“急救”方案)

application.yml 中开启:

spring:
  jpa:
    properties:
      hibernate:
        enable_lazy_load_no_trans: true

然后再次运行非事务方法:

@Test
public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() {
    SQLStatementCountValidator.reset();
    
    long docsCount = serviceLayer.countAllDocsNonTransactional();
    
    assertEquals(15, docsCount);
    SQLStatementCountValidator.assertSelectCount(6); // 假设有5个用户
}

✅ 结果:不再报错,但 SQL 执行了 6 次

  • 1 次查询所有用户
  • 5 次分别查询每个用户的文档(N+1 问题)

⚠️ 踩坑警告:即使你用了 @Fetch(FetchMode.SUBSELECT),这个配置也会绕过你的预加载策略,导致 N+1 查询!


4. 方案对比总结

方案 优点 缺点
❌ 不开启 + 事务外访问 直接抛 LazyInitializationException
✅ 不开启 + 事务内访问 高效,可控,避免 N+1 需要精心设计事务边界
⚠️ 开启 enable_lazy_load_no_trans 不用操心事务,快速修复报错 性能极差,必然触发 N+1,破坏预加载策略

关键结论:

  • 适合场景:原型开发、测试、极低频访问的单条数据(如 User.getProfile() 这种一对一)。
  • 禁止场景:生产环境、批量数据、列表页、API 接口返回嵌套集合。

Hibernate 官方文档也明确警告:

尽管开启此配置可以消除 LazyInitializationException,但更好的做法是使用合理的获取策略(fetch plan),确保在 Session 关闭前完成所有属性的初始化。


5. 最佳实践建议

别被这个“快捷方式”迷惑。真正靠谱的解决方案是:

  1. ✅ 使用 @Transactional 在 Service 层确保懒加载完成
  2. ✅ 使用 JOIN FETCH 在 JPQL 中主动预加载
  3. ✅ 使用 DTO + Projection 避免返回完整实体
  4. ✅ 使用 @EntityGraph 精细化控制加载策略

示例:用 JPQL 预加载

@Query("SELECT u FROM User u JOIN FETCH u.docs")
List<User> findAllWithDocs();

6. 结语

enable_lazy_load_no_trans 是个“止痛药”,治标不治本。它让你暂时绕过懒加载异常,但代价是性能退化和代码失控。

✅ 正确姿势:提前加载,控制事务边界
❌ 别在生产环境“图省事”开启它,否则数据库会教你做人。

所有示例代码已托管至 GitHub:https://github.com/tech-tutorial/spring-boot-hibernate-lazy-load


原始标题:Quick Guide to Hibernate enable_lazy_load_no_trans Property