Hibernate 3 升级到 Hibernate 5 后事务提交失败

最近将手头的项目由 Hibernate 3.6.10.Final 升级到 Hibernate 5.0.12.Final。运行了一段时间以后发现异常,日志内容如下

org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.
at org.springframework.orm.hibernate5.HibernateTemplate.checkWriteOperationAllowed(HibernateTemplate.java:1165)
at org.springframework.orm.hibernate5.HibernateTemplate$14.doInHibernate(HibernateTemplate.java:670)
at org.springframework.orm.hibernate5.HibernateTemplate.doExecute(HibernateTemplate.java:359)
at org.springframework.orm.hibernate5.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:326)
at org.springframework.orm.hibernate5.HibernateTemplate.update(HibernateTemplate.java:667)
at org.springframework.orm.hibernate5.HibernateTemplate.update(HibernateTemplate.java:662)

可以确定的是,相关部分的业务代码没有做过改动,那么问题一定是出在框架升级上。查看 Hibernate 源码,发现了问题的直接原因

// org.springframework.orm.hibernate5.HibernateTemplate
protected  T doExecute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");

    Session session = null;
    boolean isNew = false;
    try {
        session = getSessionFactory().getCurrentSession();
    }
    catch (HibernateException ex) {
        logger.debug("Could not retrieve pre-bound Hibernate session", ex);
    }
    if (session == null) {
        session = getSessionFactory().openSession();
        session.setFlushMode(FlushMode.MANUAL);
        isNew = true;
    }
    ...
}

可以看到,当获取不到 Session 时,Hibernate 会新建一个 session,然后设置为只读模式(FlushMode.MANUAL)。那么为什么 session 为空呢?回头看调用的地方

public class FooServiceImpl implements FooService {
    public void foo () {
        FooThread fooThread = new FooThread();
        // ...
    }

    private class FooThread extends Thread {
        public void run() { FooServiceImpl.this.update(someData); }
    }
}

fooThread 中的 fooService 对象,实际上是 FooServiceImpl.this,也就是未经 Spring 代理的对象。而我们的事务仅配置在 service 层,并没有配置在 dao 层,所以在上下文中找不到 session。

既然如此,为什么 Hibernate 3 中没有出现同样的异常呢?同样查看 Hibernate 3 的源码,发现 Spring 针对 Hibernate 3 和 5 的实现有比较大的差别

// org.springframework.orm.hibernate3.HibernateTemplate
protected  T doExecute(HibernateCallback action, boolean enforceNewSession, boolean enforceNativeSession)
            throws DataAccessException {

    Assert.notNull(action, "Callback object must not be null");

    Session session = (enforceNewSession ?
        SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession());
    ...
}

创建 session 后,并没有手动修改 FlushMode 的操作。继续深入,发现和 针对 Hibernate 5 的实现一样,创建 session 的方法实际上是
SessionFactory.openSession(),该方法创建的 session 默认是 FlushMode.AUTO 的,与 5 不同的是没有强制修改的操作。而该 flushMode 不会触发上面的异常。

找到了问题的根本原因,解决起来就非常容易了,只需要将该对象替换为被 Spring 事务代理的对象即可。

总结
  • Spring + Hibernate 5 对事务要求更严格,所有读写操作必须通过事务提交
  • 避免使用 this 在 service 层创建对象。同理,所有需要 Spring AOP 的操作都要尽量避免在类内部调用,或者使用 AspectJ 实现。
  • 在 dao 层配置事务也是不错的选择

你可能感兴趣的:(Hibernate 3 升级到 Hibernate 5 后事务提交失败)