16.3 Hibernate

我们将首先介绍Spring环境中的Hibernate 5,然后通过使用Hibernate 5来演示Spring集成O-R映射器的方法。本节将详细介绍许多问题,并显示DAO实现和事务划分的不同变体。这些模式中大多数可以直接转换为所有其他支持的ORM工具。本章中的以下部分将通过简单的例子来介绍其他ORM技术。

从Spring 5.0开始,Spring需要Hibernate ORM对JPA的支持要基于4.3或更高的版本,甚至Hibernate ORM 5.0+可以针对本机Hibernate Session API进行编程。请注意,Hibernate团队可能不会在5.0之前维护任何版本,仅仅专注于5.2以后的版本。

16.3.1 在Spring容器中配置SessionFactory

开发者可以将资源如JDBCDataSource或HibernateSessionFactory定义为Spring容器中的bean来避免将应用程序对象绑定到硬编码的资源查找上。应用对象需要访问资源的时候,都通过对应的Bean实例进行间接查找,详情可以通过下一节的DAO的定义来参考。

下面引用的应用的XML元数据定义就展示了如何配置JDBC的DataSourceHibernateSessionFactory的:

<beans>
    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
            </value>
        </property>
    </bean>
</beans>

这样,从本地的Jaksrta Commons DBCP的BasicDataSource转换到JNDI定位的DataSource仅仅只需要修改配置文件。

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

开发者也可以通过Spring的JndiObjectFactoryBean或者<jee:jndi-lookup>来获取对应Bean以访问JNDI定位的SessionFactory。但是,JNDI定位的SessionFactory在EJB上下文不常见。

16.3.2 基于Hibernate API来实现DAO

Hibernate有一个特性称之为上下文会话,在每个Hibernate本身每个事务都管理一个当前的Session。这大致相当于Spring每个事务的一个HibernateSession的同步。如下的DAO的实现类就是基于简单的Hibernate API实现的:

public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}

除了需要在实例中持有SessionFactory引用以外,上面的代码风格跟Hibernate文档中的例子十分相近。Spring团队强烈建议使用这种基于实例变量的实现风格,而非守旧的static HibernateUtil风格(总的来说,除非绝对必要,否则尽量不要使用static变量来持有资源)。

上面DAO的实现完全符合Spring依赖注入的样式:这种方式可以很好的集成Spring IoC容器,就好像Spring的HibernateTemplate代码一样。当然,DAO层的实现也可以通过纯Java的方式来配置(比如在UT中)。简单实例化ProductDaoImpl并且调用setSessionFactory(...)即可。当然,也可以使用Spring bean来进行注入,参考如下XML配置:

<beans>
    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>
</beans>

上面的DAO实现方式的好处在于只依赖于Hibernate API,而无需引入Spring的class。这从非侵入性的角度来看当然是有吸引力的,毫无疑问,这种开发方式会令Hibernate开发人员将会更加自然。

然而,DAO层会抛出Hibernate自有异常HibernateException(属于非检查异常,无需显式声明和使用try-catch),但是也意味着调用方会将异常看做致命异常——除非调用方将Hibernate异常体系作为应用的异常体系来处理。而在这种情况下,除非调用方自己来实现一定的策略,否则捕获一些诸如乐观锁失败之类的特定错误是不可能的。对于强烈基于Hibernate的应用程序或不需要对特殊异常处理的应用程序,这种代价可能是可以接受的。

幸运的是,Spring的LocalSessionFactoryBean可以通过Hibernate的SessionFactory.getCurrentSession()方法为所有的Spring事务策略提供支持,使用HibernateTransactionManager返回当前的Spring管理的事务的Session。当然,该方法的标准行为仍然是返回与正在进行的JTA事务相关联的当前Session(如果有的话)。无论开发者是使用Spring的JtaTransactionManager,EJB容器管理事务(CMT)还是JTA,都会适用此行为。

总而言之:开发者可以基于纯Hibernate API来实现DAO,同时也可以集成Spring来管理事务。

16.3.3 声明式事务划分

Spring团队建议开发者使用Spring声明式的事务支持,这样可以通过AOP事务拦截器来替代事务API的显式调用。AOP事务拦截器可以在Spring容器中使用XML或者Java的注解来进行配置。这种事务拦截器可以令开发者的代码和重复的事务代码相解耦,而开发者可以将精力更多集中在业务逻辑上,而业务逻辑才是应用的核心。

在继续之前,强烈建议开发者先查阅章节13.5 声明式事务管理的内容。

开发者可以在服务层的代码使用注解@Transactional,这样可以让Spring容器找到这些注解,以对其中注解了的方法提供事务语义。

public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }

    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }

}

开发者所需要做的就是在容器中配置PlatformTransactionManager的实现,或者是在XML中配置<tx:annotation-driver/>标签,这样就可以在运行时支持@Transactional的处理了。参考如下XML代码:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- SessionFactory, DataSource, etc. omitted -->

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <tx:annotation-driven/>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>
</beans>

16.3.4 编程式事务划分

开发者可以在应用程序的更高级别上对事务进行标定,而不用考虑低级别的数据访问执行了多少操作。这样不会对业务服务的实现进行限制;只需要定义一个Spring的PlatformTransactionManager即可。当然,PlatformTransactionManager可以从多处获取,但最好是通过setTransactionManager(..)方法以Bean来注入,正如ProductDAO应该由setProductDao(..)方法配置一样。下面的代码显示Spring应用程序上下文中的事务管理器和业务服务的定义,以及业务方法实现的示例:

<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="transactionManager" ref="myTxManager"/>
        <property name="productDao" ref="myProductDao"/>
    </bean>
</beans>
public class ProductServiceImpl implements ProductService {

    private TransactionTemplate transactionTemplate;
    private ProductDao productDao;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    public void increasePriceOfAllProductsInCategory(final String category) {
        this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            public void doInTransactionWithoutResult(TransactionStatus status) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // do the price increase...
            }
        });
    }
}

Spring的TransactionInterceptor允许任何检查的应用异常到callback代码中去,而TransactionTemplate还会非受检异常触发进行回调。TransactionTemplate则会因为非受检异常或者是由应用标记事务回滚(通过TransactionStatus)。TransactionInterceptor也是一样的处理逻辑,但是同时还允许基于方法配置回滚策略。

16.3.5 事务管理策略

无论是TransactionTemplate或者是TransactionInterceptor都将实际的事务处理代理到PlatformTransactionManager实例上来进行处理的,这个实例的实现可以是一个HibernateTransactionManager(包含一个Hibernate的SessionFactory通过使用ThreadLocalSession),也可以是JatTransactionManager(代理到容器的JTA子系统)。开发者甚至可以使用一个自定义的PlatformTransactionManager的实现。现在,如果应用有需求需要需要部署分布式事务的话,只是一个配置变化,就可以从本地Hibernate事务管理切换到JTA。简单地用Spring的JTA事务实现来替换Hibernate事务管理器即可。因为引用的PlatformTransactionManager的是通用事务管理API,事务管理器之间的切换是无需修改代码的。

对于那些跨越了多个Hibernate会话工厂的分布式事务,只需要将JtaTransactionManager和多个LocalSessionFactoryBean定义相结合即可。每个DAO之后会获取一个特定的SessionFactory引用。如果所有底层JDBC数据源都是事务性容器,那么只要使用JtaTransactionManager作为策略实现,业务服务就可以划分任意数量的DAO和任意数量的会话工厂的事务。

无论是HibernateTransactionManager还是JtaTransactionManager都允许使用JVM级别的缓存来处理Hibernate,无需基于容器的事务管理器查找,或者JCA连接器(如果开发者没有使用EJB来实例化事务的话)。

HibernateTransactionManager可以为指定的数据源的Hibernate JDBC的Connection转成为纯JDBC的访问代码。如果开发者仅访问一个数据库,则开发者完全可以不使用JTA,通过Hibernate和JDBC数据访问进行高级别事务划分。如果开发者已经通过LocalSessionFactoryBeandataSource属性与DataSource设置了传入的SessionFactoryHibernateTransactionManager会自动将Hibernate事务公开为JDBC事务。或者,开发者可以通过HibernateTransactionManagerdataSource属性的配置以确定公开事务的类型。

16.3.6 对比由容器管理的和本地定义的资源

开发者可以在不修改一行代码的情况下,在容器管理的JNDISessionFactory和本地定义的SessionFactory之间进行切换。是否将资源定义保留在容器中,还是仅仅留在应用中,都取决于开发者使用的事务策略。相对于Spring定义的本地SessionFactory来说,手动注册的JNDISessionFactory没有什么优势。通过Hibernate的JCA连接器来发布一个SessionFactory只会令代码更符合J2EE服务标准,但是并不会带来任何实际的价值。

Spring对事务支持不限于容器。使用除JTA之外的任何策略配置,事务都可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下,Spring的单一资源本地事务支持是一种轻量级和强大的替代JTA的解决方案。当开发者使用本地EJB无状态会话Bean来驱动事务时,即使只访问单个数据库,并且只使用无状态会话Bean来通过容器管理的事务来提供声明式事务,开发者的代码依然是依赖于EJB容器和JTA的。同时,以编程方式直接使用JTA也需要一个J2EE环境的。 JTA不涉及JTA本身和JNDI DataSource实例方面的容器依赖关系。对于非Spring,JTA驱动的Hibernate事务,开发者必须使用Hibernate JCA连接器或开发额外的Hibernate事务代码,并为JVM级缓存正确配置TransactionManagerLookup

Spring驱动的事务可以与本地定义的HibernateSessionFactory一样工作,就像本地JDBC DataSource访问单个数据库一样。但是,当开发者有分布式事务的要求的情况下,只能选择使用Spring JTA事务策略。JCA连接器是需要特定容器遵循一致的部署步骤的,而且显然JCA支持是需要放在第一位的。JCA的配置需要比部署本地资源定义和Spring驱动事务的简单web应用程序需要更多额外的的工作。同时,开发者还需要使用容器的企业版,比如,如果开发者使用的是WebLogic Express的非企业版,就是不支持JCA的。具有跨越单个数据库的本地资源和事务的Spring应用程序适用于任何基于J2EE的Web容器(不包括JTA,JCA或EJB),如Tomcat,Resin或甚至是Jetty。此外,开发者可以轻松地在桌面应用程序或测试套件中重用中间层代码。

综合前面的叙述,如果不使用EJB,请尽量使用本地的SessionFactory设置和Spring的HibernateTransactionManagerJtaTransactionManager。开发者能够得到了前面提到的所有好处,包括适当的事务性JVM级缓存和分布式事务支持,而且没有容器部署的不便。只有必须配合EJB使用的时候,JNDI通过JCA连接器来注册HibernateSessionFactory才有价值。

16.3.7 Hibernate的虚假应用服务器警告

在某些具有非常严格的XADataSource实现的JTA环境(目前只有一些WebLogic Server和WebSphere版本)中,当配置Hibernate时,没有考虑到JTA的 PlatformTransactionManager对象,可能会在应用程序服务器日志中显示虚假警告或异常。这些警告或异常经常描述正在访问的连接不再有效,或者JDBC访问不再有效。这通常可能是因为事务不再有效。例如,这是WebLogic的一个实际异常:

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

开发者可以通过配置令Hibernate意识到Spring中同步的JTAPlatformTransactionManager实例的存在,这样即可消除掉前面所说的虚假警告信息。开发者有以下两种选择:

  • 如果在应用程序上下文中,开发者已经直接获取了JTA PlatformTransactionManager对象(可能是从JNDI到JndiObjectFactoryBean或者<jee:jndi-lookup>标签),并将其提供给Spring的JtaTransactionManager(其中最简单的方法就是指定一个引用bean将此JTA PlatformTransactionManager实例定义为LocalSessionFactoryBeanjtaTransactionManager属性的值)。 Spring之后会令PlatformTransactionManager对象对Hibernate可见。
  • 更有可能开发者无法获取JTAPlatformTransactionManager实例,因为Spring的JtaTransactionManager是可以自己找到该实例的。因此,开发者需要配置Hibernate令其直接查找JTA PlatformTransactionManager。开发者可以如Hibernate手册中所述那样通过在Hibernate配置中配置应用程序服务器特定的TransactionManagerLookup类来执行此操作。

本节的其余部分描述了在PlatformTransactionManager对Hibernate可见和PlatformTransactionManager对Hibernate不可见的情况下发生的事件序列:

当Hibernate未配置任何对JTAPlatformTransactionManager的进行查找时,JTA事务提交时会发生以下事件:

  • JTA事务提交
  • Spring的JtaTransactionManager与JTA事务同步,所以它被JTA事务管理器通过afterCompletion回调调用。
  • 在其他活动中,此同步令Spring通过Hibernate的afterTransactionCompletion触发回调(用于清除Hibernate缓存),然后在Hibernate Session上调用close(),从而令Hibernate尝试close()JDBC连接。
  • 在某些环境中,因为事务已经提交,应用程序服务器会认为Connection不可用,导致Connection.close()调用会触发警告或错误。

当Hibernate配置了对JTAPlatformTransactionManager进行查找时,JTA事务提交时会发生以下事件:

  • JTA事务准备提交
  • Spring的JtaTransactionManager与JTA事务同步,所以JTA事务管理器通过beforeCompletion方法来回调事务。
  • Spring确定Hibernate与JTA事务同步,并且行为与前一种情况不同。假设Hibernate Session需要关闭,Spring将会关闭它。
  • JTA事务提交。
  • Hibernate与JTA事务同步,所以JTA事务管理器通过afterCompletion方法回调事务,可以正确清除其缓存。