Spring ORM数据访问——Hibernate

Posted EthanPark

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring ORM数据访问——Hibernate相关的知识,希望对你有一定的参考价值。

Hibernate

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

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

在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。但是,EJB上下文通常不常见。

基于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支持任何Spring事务策略的Hibernate的SessionFactory.getCurrentSession()方法,即使使用HibernateTransactionManager返回当前的Spring管理的事务Session。当然,该方法的标准行为仍然返回与正在进行的JTA事务相关联的当前Session(如果有的话)。无论开发者是使用Spring的JtaTransactionManager,EJB容器管理事务(CMT)还是JTA,都会适用此行为。

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

声明式事务划分

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>

编程式事务划分

开发者可以在应用程序的更高级别上对事务进行标定,在这样的低级别数据访问服务之上跨越任意数量的操作。而不对业务服务的实现进行限制;它只需要定义一个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也是一样的处理逻辑,但是同时还允许基于方法配置回滚策略。

事务管理策略

无论是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属性的配置以确定公开事务的类型。

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

开发者可以在不修改一行代码的情况下,在容器管理的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事务代码,并将TransactionManagerLookup配置为正确的JVM级缓存。

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才有价值。

Hibernate的虚假应用服务器警告

In some JTA environments with very strict XADataSource implementations — currently only some WebLogic Server and WebSphere versions — when Hibernate is configured without regard to the JTA PlatformTransactionManager object for that environment, it is possible for spurious warning or exceptions to show up in the application server log. These warnings or exceptions indicate that the connection being accessed is no longer valid, or JDBC access is no longer valid, possibly because the transaction is no longer active. As an example, here is an actual exception from WebLogic:

在某些具有非常严格的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方法回调事务,可以正确清除其缓存。

以上是关于Spring ORM数据访问——Hibernate的主要内容,如果未能解决你的问题,请参考以下文章

Spring ORM数据访问——Hibernate

Spring ORM数据访问——概述

Spring ORM数据访问——概述

Spring ORM数据访问——概述

Spring ORM数据访问——JPA

Spring ORM数据访问——JPA