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

为何添加@Transactional才能初始化CriteriaDAO懒加载集合?是否有更优方案?

问题描述

我通过Session对象构建CriteriaBuilder获取Document对象,该对象包含两个一对多关联列表。明明Session处于开启状态,执行Hibernate.initialize初始化这两个列表时却报错「Session could not initialize proxy - no Session」。给方法添加@Transactional(Transactional.TxType.REQUIRED)能解决问题,但我有两个疑问:

  1. 加这个注解会新建数据库连接吗?会不会造成资源浪费?
  2. 有没有更优的解决方案?

相关代码如下:

@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 18:51:10