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
相关产品推荐
相关产品推荐

