You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot测试类如何手动创建Hibernate Session解决懒加载异常

Spring JPA测试中懒加载LazyInitializationException解决方案

问题场景

Board实体的columns集合配置为LAZY懒加载策略,测试列关联到看板的逻辑时,出现两类问题:

  • 无事务时运行测试,抛出org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role could not initialize proxy - no Session异常
  • 给测试方法加@Transactional后异常消失,但同一测试方法内多次查询无法拿到数据库最新状态,断言逻辑失效
  • 要求不修改关联加载策略为EAGER,不接受手动打开Session的无效方案

原测试代码如下:

@Test
void testAddColumn() {
    Board board = new Board("title", groupId);
    board = boardRepository.save(board);

    assertTrue(board.getColumns().isEmpty());

    Column column = new Column(board, "column title", 2);
    column = boardService.addColumn(board.getId(), column);

    
    assertEquals(1, boardRepository.findById(board.getId()).get().getColumns().size()); // 该行抛出异常

    Column column2 = new Column(board, "column title2", 1);
    boardService.addColumn(board.getId(), column2);
    assertEquals(2, boardRepository.findById(board.getId()).get().getColumns().size());

    boardService.delete(board.getId());
    
}

问题根因

  • 无事务场景下,Spring Data JPA的每个Repository调用都会使用独立Session,调用结束后Session立即关闭。后续调用getColumns()触发懒加载时,实体关联的Session已经销毁,直接抛出异常。
  • 手动调用sessionFactory.openSession()无效:手动打开的Session和Spring管理的、用于Repository操作的线程绑定Session完全独立,查询返回的代理实体绑定的是已关闭的旧Session,新打开的Session不会接管旧实体的代理初始化逻辑。
  • 加@Transactional后逻辑失效的原因:整个测试方法处于同一持久化上下文(一级缓存)中,多次调用findById()查询同ID实体时,会直接返回内存中缓存的旧实例,不会查询数据库获取addColumn操作后的最新数据,导致断言拿到的是未更新的集合状态。

可行解决方案

方案1:事务测试+主动刷新清空持久化上下文(改动最小)

给测试方法加@Transactional保证Session贯穿整个测试生命周期,每次写操作后强制刷盘并清空一级缓存,让后续查询直接走数据库获取最新数据,同时可正常触发懒加载。

@Autowired
private EntityManager entityManager;

@Test
@Transactional
void testAddColumn() {
    Board board = new Board("title", groupId);
    board = boardRepository.save(board);
    // 保存后刷盘并清空缓存,避免后续查询命中旧实例
    entityManager.flush();
    entityManager.clear();

    assertTrue(board.getColumns().isEmpty());

    Column column = new Column(board, "column title", 2);
    column = boardService.addColumn(board.getId(), column);
    // 每次写操作后重复刷盘清缓存逻辑
    entityManager.flush();
    entityManager.clear();
    
    assertEquals(1, boardRepository.findById(board.getId()).get().getColumns().size());

    Column column2 = new Column(board, "column title2", 1);
    boardService.addColumn(board.getId(), column2);
    entityManager.flush();
    entityManager.clear();

    assertEquals(2, boardRepository.findById(board.getId()).get().getColumns().size());

    boardService.delete(board.getId());
}

注意:Spring测试环境下@Transactional默认会在方法执行结束后自动回滚事务,不会污染测试数据库数据,无需额外处理回滚逻辑。

方案2:自定义Fetch Join查询(最推荐,生产环境通用)

在BoardRepository中编写自定义查询,通过JOIN FETCH语法在查询Board时直接加载关联的columns集合,从根源上避免懒加载依赖Session的问题,不需要加事务即可正常运行。
首先在Repository接口添加自定义查询方法:

public interface BoardRepository extends JpaRepository<Board, Long> {
    // 查询时关联抓取columns集合,返回的Board实例中columns已完成初始化
    @Query("SELECT b FROM Board b LEFT JOIN FETCH b.columns WHERE b.id = :boardId")
    Optional<Board> findByIdWithColumns(Long boardId);
}

将测试代码中所有findById调用替换为该自定义方法即可:

@Test
void testAddColumn() {
    Board board = new Board("title", groupId);
    board = boardRepository.save(board);

    assertTrue(board.getColumns().isEmpty());

    Column column = new Column(board, "column title", 2);
    column = boardService.addColumn(board.getId(), column);

    assertEquals(1, boardRepository.findByIdWithColumns(board.getId()).get().getColumns().size());

    Column column2 = new Column(board, "column title2", 1);
    boardService.addColumn(board.getId(), column2);
    assertEquals(2, boardRepository.findByIdWithColumns(board.getId()).get().getColumns().size());

    boardService.delete(board.getId());
}

该方案同时避免了懒加载产生的额外N+1查询问题,性能更优,是业务代码中处理关联查询的标准实践。

方案3:事务测试+主动分离已查询实体

如果不想每次清空整个持久化上下文,可以在每次写操作后,将持有的旧Board实体从持久化上下文中分离(detach),后续查询时一级缓存中无对应实体会直接走数据库查询最新数据,同时事务内Session保持打开状态,懒加载可正常执行。

@Autowired
private EntityManager entityManager;

@Test
@Transactional
void testAddColumn() {
    Board board = new Board("title", groupId);
    board = boardRepository.save(board);
    entityManager.detach(board); // 分离已保存的实体,清除缓存中对应条目

    Board currentBoard = boardRepository.findById(board.getId()).get();
    assertTrue(currentBoard.getColumns().isEmpty());

    Column column = new Column(currentBoard, "column title", 2);
    boardService.addColumn(currentBoard.getId(), column);
    entityManager.detach(currentBoard); // 操作后分离旧的查询实例
    
    currentBoard = boardRepository.findById(board.getId()).get();
    assertEquals(1, currentBoard.getColumns().size());

    Column column2 = new Column(currentBoard, "column title2", 1);
    boardService.addColumn(currentBoard.getId(), column2);
    entityManager.detach(currentBoard);

    currentBoard = boardRepository.findById(board.getId()).get();
    assertEquals(2, currentBoard.getColumns().size());

    boardService.delete(board.getId());
}

内容的提问来源于stack exchange,提问作者Rafael Lima

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 23:30:09