bigywater.tech
Spring · 157

LazyInitializationException:几种修法的代价对比

LazyInitializationException 有哪几种修法?各自的 SQL 条数、连接占用和代价是多少?open-in-view 和 enable_lazy_load_no_trans 能不能当解药?这篇在 Spring Boot 4.0.8 上用同一份数据逐个测。

6 条
直接访问 5 个 Parent 的 children(N+1)执行的 SQL 条数;join fetch、@EntityGraph、DTO 投影都是 1 条
11 条
open-in-view=false 且 enable_lazy_load_no_trans=true 时同样的访问执行的 SQL 条数,打开 6 个 Session
active=1
默认 open-in-view=true 时,方法返回前一刻连接池的活跃连接数;关掉 open-in-view 后是 0

默认

实测 · 本机执行输出
  Controller 里直接 findAll() 再访问 children(没有事务):成功,统计到子对象 20 个 | 执行 SQL 6 条,加载实体 25 个,打开 Session 0 个,返回前连接池 active=1
  Service 方法加 @Transactional(readOnly = true),里面访问 children:成功,统计到子对象 20 个 | 执行 SQL 6 条,加载实体 25 个,打开 Session 0 个,返回前连接池 active=1
  join fetch 查询:成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 0 个,返回前连接池 active=1
  @EntityGraph(attributePaths = "children"):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 0 个,返回前连接池 active=1
  DTO 投影(new ParentDto(id, name, count(c))):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 0 个,打开 Session 0 个,返回前连接池 active=1
  启动日志里有没有 open-in-view 的警告:True

open-in-view=true:不报错,但连接被占着,N+1 是 6 条 SQL。

关闭 OSIV

实测 · 本机执行输出
  Controller 里直接 findAll() 再访问 children(没有事务):异常 LazyInitializationException:Cannot lazily initialize collection of role 'demo.Parent.children' with key '1' (no session) | 执行 SQL 1 条,加载实体 5 个,打开 Session 1 个,返回前连接池 active=0
  Service 方法加 @Transactional(readOnly = true),里面访问 children:成功,统计到子对象 20 个 | 执行 SQL 6 条,加载实体 25 个,打开 Session 1 个,返回前连接池 active=0
  join fetch 查询:成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 1 个,返回前连接池 active=0
  @EntityGraph(attributePaths = "children"):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 3 个,返回前连接池 active=0
  DTO 投影(new ParentDto(id, name, count(c))):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 0 个,打开 Session 1 个,返回前连接池 active=0

直接访问抛 no session;事务方法 6 条,join fetch、EntityGraph、DTO 1 条。

lazy_load_no_trans

实测 · 本机执行输出
  Controller 里直接 findAll() 再访问 children(没有事务):成功,统计到子对象 20 个 | 执行 SQL 11 条,加载实体 30 个,打开 Session 6 个,返回前连接池 active=0
  Service 方法加 @Transactional(readOnly = true),里面访问 children:成功,统计到子对象 20 个 | 执行 SQL 6 条,加载实体 25 个,打开 Session 1 个,返回前连接池 active=0
  join fetch 查询:成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 1 个,返回前连接池 active=0
  @EntityGraph(attributePaths = "children"):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 25 个,打开 Session 3 个,返回前连接池 active=0
  DTO 投影(new ParentDto(id, name, count(c))):成功,统计到子对象 20 个 | 执行 SQL 1 条,加载实体 0 个,打开 Session 1 个,返回前连接池 active=0

压住异常,但 11 条 SQL、6 个 Session。

分步演示

LazyInit
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Boot 4.0.8,Hibernate 7.2,H2 内存库;5 个 Parent 各 4 个 Child;Hibernate 统计信息 generate_statistics 的 prepareStatementCount、entityLoadCount、sessionOpenCount;连接池 active 取自 HikariCP 的 MXBean;每种配置启动一次应用后依次访问五个端点)
    • 没有验证:MySQL 上连接被 OSIV 占着是否同样表现;并发请求下连接池耗尽的程度(见 HikariCP 一篇);分页加 join fetch 的问题(见 Hibernate 7 一篇);@BatchSize、hibernate.default_batch_fetch_size 对 N+1 的缓解;Spring Data 的 Projection 接口;WebFlux 下的行为;Session 数量在不同查询方式下为何不同(只记录了统计值)
    京ICP备2026067502号-1京公网安备11010502063487号