Spring5.1.x官方参考指南中文版

 主页   资讯   文章   代码   电子书 

4.3Hibernate

我们从对Spring环境中的Hibernate 5的介绍开始,用它来演示Spring用于集成OR映射器的方法。 本节详细讨论了许多问题,并展示了DAO实现和事务划分的不同变体。 这些模式中的大多数都可以直接转换为所有其他受支持的ORM工具。 然后,本章后面的部分将介绍其他ORM技术,并显示一些简短的示例。

从Spring Framework 5.0开始,Spring需要Hibernate ORM 4.3或更高版本才能提供JPA支持,甚至需要Hibernate ORM 5.0+才能针对本机Hibernate Session API进行编程。 请注意,Hibernate团队不再维护5.1之前的任何版本,并且可能很快会专注于5.3+。

4.3.1 Spring容器中的SessionFactory设置

为了避免将应用程序对象与硬编码的资源查找绑定在一起,可以在Spring容器中将资源(例如JDBC DataSource或Hibernate SessionFactory)定义为bean。 需要访问资源的应用程序对象通过bean引用接收对此类预定义实例的引用,如下一节中的DAO定义所示。

以下XML应用程序上下文定义摘录显示了如何在其上设置JDBC数据源和Hibernate SessionFactory:

<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>

从本地Jakarta Commons DBCP BasicDataSource切换到位于JNDI的DataSource(通常由应用程序服务器管理)仅是配置问题,如以下示例所示:

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

您还可以使用Spring的JndiObjectFactoryBean / <jee:jndi-lookup>检索并公开它,从而访问位于JNDI的SessionFactory。但是,这通常在EJB上下文之外并不常见。

Spring还提供了LocalSessionFactoryBuilder变体,可与@Bean样式配置和编程设置无缝集成(不涉及FactoryBean)。

LocalSessionFactoryBean和LocalSessionFactoryBuilder都支持后台引导,并且Hibernate初始化与给定引导执行程序(例如SimpleAsyncTaskExecutor)上的应用程序引导线程并行运行。在LocalSessionFactoryBean上,可以通过bootstrapExecutor属性使用。在程序化LocalSessionFactoryBuilder上,有一个重载的buildSessionFactory方法,该方法带有引导执行程序参数。

从Spring Framework 5.1开始,此类native Hibernate设置还可以在native Hibernate访问时候公开用于标准JPA交互的JPA EntityManagerFactory。有关详细信息,请参见JPA的nativeHibernate设置。

4.3.2 基于Plain Hibernate API实现DAO

Hibernate具有称为上下文会话的功能,其中,Hibernate本身在每个事务中管理一个当前会话。 这大致相当于Spring在每个事务中同步一个Hibernate Session。 基于简单的Hibernate API,相应的DAO实现类似于以下示例:

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参考文档和示例类似。 我们强烈建议您在Hibernate的CaveatEmptor示例应用程序中的老式静态HibernateUtil类上使用基于实例的设置。 (通常,除非绝对必要,否则不要在静态变量中保留任何资源。)

前面的DAO示例遵循依赖项注入模式。 就像针对Spring的HibernateTemplate进行编码一样,它非常适合Spring IoC容器。 您还可以在纯Java中设置这种DAO(例如,在单元测试中)。 为此,将其实例化并使用所需的工厂引用调用setSessionFactory(..)。 作为Spring bean的定义,DAO类似于以下内容:

<beans>

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

</beans>

这种DAO样式的主要优点是它仅依赖于Hibernate API。不需要导入任何Spring类。从非侵入性的角度来看,这很有吸引力,并且对于Hibernate开发人员而言可能更自然。

但是,DAO抛出普通的HibernateException(未经检查,因此不必声明或捕获),这意味着调用方只能将异常视为一般致命的消息-除非他们希望依赖于Hibernate自己的异常层次结构。如果不将调用者与实现策略联系在一起,则无法捕获特定原因(例如乐观锁定失败)。这种权衡对于基于Hibernate的应用程序,不需要任何特殊异常处理或两者都可以接受。

幸运的是,Spring的LocalSessionFactoryBean支持任何Spring交易策略的Hibernate的SessionFactory.getCurrentSession()方法,即使使用HibernateTransactionManager,也返回当前的Spring管理的交易会话。该方法的标准行为仍然是返回与正在进行的JTA事务相关联的当前Session(如果有)。无论您使用Spring的JtaTransactionManager,EJB容器管理的交易(CMT)还是JTA,此行为均适用。

总之,您可以基于普通的Hibernate API实现DAO,同时仍然能够参与Spring管理的事务。

4.3.3 声明式事务划分

我们建议您使用Spring的声明式事务支持,该支持使您可以将Java代码中的显式事务划分API调用替换为AOP事务拦截器。 您可以使用Java批注或XML在Spring容器中配置此事务拦截器。 这种声明式事务处理功能使您可以使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的真正价值。

在继续之前,我们强烈建议您阅读声明式事务管理(如果您尚未阅读的话)。

您可以使用@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实现(作为bean)和<tx:annotation-driven />条目,并在运行时选择@Transactional处理。 以下示例显示了如何执行此操作:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://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>

4.3.4 程序化交易划分

您可以在应用程序的更高级别中划分事务,在跨越任意数量的操作的较低级别数据访问服务之上。 对周围业务服务的实施也没有限制。 它只需要一个Spring PlatformTransactionManager。 同样,后者可以来自任何地方,但最好通过setTransactionManager(..)方法作为bean的引用。 另外,应通过setProductDao(..)方法设置productDAO。 以下几对代码片段显示了Spring应用程序上下文中的事务管理器和业务服务定义,以及业务方法实现的示例:

<beans>

    <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允许将任何已检查的应用程序异常与回调代码一起引发,而TransactionTemplate仅限于回调中未检查的异常。 如果未检查的应用程序异常或应用程序将事务标记为仅回滚(通过设置TransactionStatus),则TransactionTemplate会触发回滚。 默认情况下,TransactionInterceptor的行为方式相同,但允许每种方法配置可回滚策略。

4.3.5 交易管理策略

TransactionTemplate和TransactionInterceptor都将实际的事务处理委托给PlatformTransactionManager实例(对于Hibernate,可以通过使用ThreadLocal会话将其委托给HibernateTransactionManager(对于单个Hibernate SessionFactory))或JtaTransactionManager(委托给容器的JTA子系统)。应用程序。您甚至可以使用自定义PlatformTransactionManager实现。从本机Hibernate事务管理切换到JTA(例如,面对应用程序的某些部署的分布式事务要求时)仅是配置问题。您可以将Hibernate事务管理器替换为Spring的JTA事务实现。事务划分和数据访问代码都无需更改即可工作,因为它们使用通用的事务管理API。

对于跨多个Hibernate会话工厂的分布式事务,可以将JtaTransactionManager作为具有多个LocalSessionFactoryBean定义的事务策略进行组合。然后,每个DAO都会将一个特定的SessionFactory引用传递到其相应的bean属性中。如果所有基础JDBC数据源都是事务性容器数据源,那么只要使用JtaTransactionManager作为策略,业务服务就可以在任何数量的DAO和任何会话工厂之间划分事务,而无需特别注意。

HibernateTransactionManager和JtaTransactionManager都允许使用Hibernate进行正确的JVM级别的缓存处理,而无需特定于容器的事务管理器查找或JCA连接器(如果您不使用EJB来初始化事务)。

HibernateTransactionManager可以将Hibernate JDBC连接导出到特定DataSource的纯JDBC访问代码。此功能允许使用混合的Hibernate和JDBC数据访问进行高级事务划分,而无需JTA,前提是您仅访问一个数据库。如果已通过LocalSessionFactoryBean类的dataSource属性设置了传入的SessionFactory和DataSource,则HibernateTransactionManager会将Hibernate事务自动公开为JDBC事务。另外,您可以通过HibernateTransactionManager类的dataSource属性显式指定应该为其公开事务的DataSource。

4.3.6 比较容器管理的资源和本地定义的资源

您可以在容器管理的JNDI SessionFactory和本地定义的JNDI SessionFactory之间切换,而无需更改单行应用程序代码。将资源定义保留在容器中还是在应用程序中本地保留,主要取决于您使用的事务策略。与Spring定义的本地SessionFactory相比,手动注册的JNDI SessionFactory没有任何好处。通过Hibernate的JCA连接器部署SessionFactory可以增加参与Java EE服务器的管理基础结构的附加价值,但是并不能增加任何实际价值。

Spring的交易支持不限于容器。当使用除JTA之外的任何其他策略配置时,事务支持还可以在独立或测试环境中工作。尤其是在单数据库事务的典型情况下,Spring的单资源本地事务支持是JTA的轻量级功能强大的替代方案。当您使用本地EJB无状态会话Bean驱动事务时,即使您仅访问单个数据库并且仅使用无状态会话Bean通过容器管理的事务来提供声明性事务,也要依赖EJB容器和JTA。以编程方式直接使用JTA还需要Java EE环境。就JTA本身和JNDI DataSource实例而言,JTA不仅仅涉及容器依赖项。对于非Spring,JTA驱动的Hibernate事务,您必须使用Hibernate JCA连接器或额外的Hibernate事务代码,并将TransactionManagerLookup配置为正确的JVM级别的缓存。

如果Spring访问的事务访问单个数据库,则它们可以与本地定义的Hibernate SessionFactory一起使用,也可以与本地JDBC数据源一起使用。因此,只有在分配了交易需求时才需要使用Spring的JTA交易策略。 JCA连接器需要特定于容器的部署步骤,并且首先需要(显然)JCA支持。与部署具有本地资源定义和Spring驱动的事务的简单Web应用程序相比,此配置需要更多的工作。另外,如果使用(例如)不提供JCA的WebLogic Express,则通常需要容器的企业版。具有跨单个数据库的本地资源和事务的Spring应用程序可以在任何Java EE Web容器(没有JTA,JCA或EJB)中工作,例如Tomcat,Resin甚至普通Jetty。此外,您可以轻松地在桌面应用程序或测试套件中重用这样的中间层。

考虑到所有因素,如果您不使用EJB,请坚持使用本地SessionFactory设置和Spring的HibernateTransactionManager或JtaTransactionManager。您将获得所有好处,包括适当的事务性JVM级别的缓存和分布式事务,而不会给容器部署带来不便。通过JCA连接器对Hibernate SessionFactory进行JNDI注册仅在与EJB结合使用时才增加价值。

4.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知道与之同步的JTA PlatformTransactionManager实例(以及Spring实例)来解决此警告。您可以通过以下两种方式执行此操作:

  • 如果在您的应用程序上下文中,您已经直接获取了JTA PlatformTransactionManager对象(大概是通过Jndi通过JndiObjectFactoryBean或<jee:jndi-lookup>从JNDI获取)并将其提供给例如Spring的JtaTransactionManager,则最简单的方法是指定对将此JTA PlatformTransactionManager实例定义为LocalSessionFactoryBean的jtaTransactionManager属性的值的bean。然后,Spring使该对象可用于Hibernate。
  • 很可能您还没有JTA PlatformTransactionManager实例,因为Spring的JtaTransactionManager可以自行找到它。因此,您需要配置Hibernate以直接查找JTA PlatformTransactionManager。您可以通过在Hibernate配置中配置特定于应用程序服务器的TransactionManagerLookup类来实现此目的,如Hibernate手册中所述。

本节的其余部分描述了在Hibernate不了解JTA PlatformTransactionManager的情况下发生的事件序列。

当未对JTA ​​PlatformTransactionManager进行任何意识的Hibernate配置时,在JTA事务提交时会发生以下事件:

  • JTA事务提交。
  • Spring的JtaTransactionManager已同步到JTA事务,因此JTA事务管理器通过afterCompletion回调对其进行回调。
  • 在其他活动中,此同步可以通过Spring的Hibernate的afterTransactionCompletion回调(用于清除Hibernate缓存)触发Spring到Hibernate的回调,然后在Hibernate会话上进行显式的close()调用,这将导致Hibernate尝试进行close() JDBC连接。
  • 在某些环境中,此Connection.close()调用随后触发警告或错误,因为应用程序服务器不再认为Connection可用,因为事务已被提交。

当Hibernate配置为具有JTA PlatformTransactionManager意识时,在JTA事务提交时会发生以下事件:

  • JTA事务已准备好提交。
  • Spring的JtaTransactionManager已同步到JTA事务,因此JTA事务管理器通过beforeCompletion回调来调用该事务。
  • Spring意识到,Hibernate本身已同步到JTA事务,并且其行为与以前的场景不同。假设完全需要关闭Hibernate会话,Spring现在将其关闭。
  • JTA事务提交。
  • Hibernate已同步到JTA事务,因此JTA事务管理器通过afterCompletion回调来调用该事务,并且可以正确清除其缓存。