为何添加@Transactional才能初始化CriteriaDAO懒加载集合?是否有更优方案?
我通过Session对象构建CriteriaBuilder获取Document对象,该对象包含两个一对多关联列表。明明Session处于开启状态,执行Hibernate.initialize初始化这两个列表时却报错「Session could not initialize proxy - no Session」。给方法添加@Transactional(Transactional.TxType.REQUIRED)能解决问题,但我有两个疑问:
- 加这个注解会新建数据库连接吗?会不会造成资源浪费?
- 有没有更优的解决方案?
相关代码如下:
@Autowired protected Session session; .... @Override public Document find(Long id, boolean close) { try { CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); CriteriaQuery<Document> queryDocument = criteriaBuilder.createQuery(Document.class); Root<Document> rootDocument = queryDocument.from(Document.class); queryDocument.select(rootDocument); queryDocument.where(criteriaBuilder.equal(rootDocument.get("id"), id)); TypedQuery<Document> query = session.createQuery(queryDocument); Document singleResult = query.getSingleResult(); System.out.println(session.isOpen()); //Returns true //Initialize other lists (one to many) Hibernate.initialize(singleResult.getRevisions()); Hibernate.initialize(singleResult.getFiles()); // return singleResult; } catch (Exception ex) { throw ex; } finally { if (session != null && close) { session.close(); } } }
为什么加@Transactional能解决问题
虽然你看到Session是开启状态,但Hibernate的Session和事务是绑定的——如果没有活跃事务,Session会处于「自动提交」模式,执行完getSingleResult()后,Hibernate会自动flush并关闭Session的持久化上下文,导致后续初始化关联列表时,代理对象找不到可用的Session。加上@Transactional后,事务会覆盖Session的自动提交行为,保持持久化上下文直到方法执行完毕,所以初始化操作能正常进行。
关于数据库连接的疑问
@Transactional(TxType.REQUIRED)是默认的事务传播级别:
- 如果当前已经存在活跃事务,会复用该事务,不会新建数据库连接;
- 如果没有活跃事务,才会新建一个事务并获取对应的数据库连接。
只要你的方法调用时机是在已有事务的上下文里,就不会额外消耗连接资源,不用担心浪费问题。
更优解决方案
方案1:使用JOIN FETCH提前加载关联列表
在Criteria查询中直接通过JOIN FETCH一次性加载关联的列表,避免后续手动初始化,这是最推荐的方式,能减少数据库查询次数(解决N+1问题):
// 在查询中添加JOIN FETCH rootDocument.fetch("revisions", JoinType.LEFT); rootDocument.fetch("files", JoinType.LEFT); queryDocument.select(rootDocument); queryDocument.where(criteriaBuilder.equal(rootDocument.get("id"), id)); // 注意:如果关联列表是集合,需要distinct避免重复结果 queryDocument.distinct(true);
这样查询出来的Document对象,关联的revisions和files已经被加载完成,不需要再调用Hibernate.initialize,也不会依赖Session的持久化上下文。
方案2:手动控制事务边界
如果不想用JOIN FETCH,也可以在方法内部手动开启和提交事务,确保初始化操作在事务上下文内执行:
@Override public Document find(Long id, boolean close) { Transaction tx = null; try { tx = session.beginTransaction(); // 原查询代码... Hibernate.initialize(singleResult.getRevisions()); Hibernate.initialize(singleResult.getFiles()); tx.commit(); return singleResult; } catch (Exception ex) { if (tx != null) tx.rollback(); throw ex; } finally { if (session != null && close) { session.close(); } } }
这种方式和添加@Transactional效果类似,但手动管理事务更灵活,适合不需要Spring事务管理的场景。
方案3:调整关联的加载策略(不推荐)
如果这两个关联列表在大多数场景下都需要被加载,可以直接修改实体类的关联注解,设置fetch = FetchType.EAGER:
@OneToMany(mappedBy = "document", fetch = FetchType.EAGER) private List<Revision> revisions; @OneToMany(mappedBy = "document", fetch = FetchType.EAGER) private List<File> files;
但不推荐这种方式,因为EAGER加载会导致所有查询Document的操作都自动加载关联列表,可能造成不必要的性能开销,除非确实是全局需要的场景。
内容的提问来源于stack exchange,提问作者KenobiBastila

