使用ORM工具进行数据访问

Chapter 12. 使用ORM工具进行数据访问

12.1. 简介

Spring在资源管理,DAO实现支持以及事务策略等方面提供了与 Hibernate、JDO、Oracle TopLink、iBATIS SQL Mappings 以及 JPA 的集成。以Hibernate为例,Spring通过使用许多IoC的便捷特性对它提供了一流的支持,帮助你处理很多典型的Hibernate整合的问题。所有的这些支持,都遵循Spring通用的事务和DAO异常体系。通常来说有两种不同的整合风格:你可以使用Spring提供的DAO模板,或者直接使用Hibernate/JDO/TopLink等工具的原生API编写DAO。无论采取哪种风格,这些DAO都可以通过IoC进行配置,并参与到Spring的资源和事务管理中去。

当你选择使用O/R Mapping工具来创建数据访问应用程序的时候,Spring为你提供了重大的支持。首先你应该了解的是,一旦使用了Spring对O/R Mapping的支持,你不需要亲自做所有的事情。无论如何,在决定花费力气并冒着风险去构造类似的内部底层架构之前,我们都建议你考虑和使用Spring的解决方案。无论您采用何种技术,Spring对大部分O/R Mapping的支持都可以以library的形式被调用,因为所有的内容都被设计成一组可重用的JavaBeans。在Spring的IoC容器中使用这些类,更是有着配置和部署简单的好处。因而,在这一章中你看到的大多数例子都是在Spring的 ApplicationContext 中进行的配置。

使用Spring构建你的O/R Mapping DAO的好处包括:

  • 测试简单。 Spring的IoC使得替换不同的实现和配置变得非常简单,这些内容包括:Hibernate SessionFactory 的位置,JDBC DataSource,事务管理器以及映射对象的实现(如果需要)等。这样也就很容易隔离并测试持久化相关的代码的各个部分。

  • 异常封装。 Spring能够封装你所选择的O/R Mapping工具所抛出的异常,将它们从专有的、潜在的checked exception转化为一组抽象的runtime DataAccessException体系。这可以使你仅需要在恰当的应用程序层次去处理大部分不可恢复的持久层异常,从而避免了很多令人讨厌的catch/throw以及异常声明。当然,你还是可以在你需要的地方捕捉和处理异常。回想一下JDBC异常(包括与DB相关的Dialect)被转变为同样的异常体系,这就意味着你可以在一致的编程模型中处理JDBC操作。

  • 通用的资源管理。 Spring的application context能够处理诸如Hibernate的 SessionFactory, JDBC的 DataSource,iBatis的SQL Maps配置对象以及其他相关资源的定位和配置。这样,这些配置的值很容易被管理和修改。Spring提供了简单、有效、安全的对持久层资源的处理。以Hibernate为例,通常在使用Hibernate时,需要使用同一个Hibernate Session 对象以确保高效和恰当地事务处理。 Spring让我们能够很容易透明地创建并绑定一个 Session 到当前线程。你可以使用以下两种办法之一:通过使用一个外部的template包装类在Java代码层次实现,或者通过Hibernate的 SessionFactory 暴露当前 Session 对象(对于那些建立在Hibernate3原生的API上的DAO)。这样,对于任何的事务环境(本地事务或者JTA),Spring解决了许多在Hibernate使用中不断出现的这样那样的问题。

  • 综合的事务管理。 Spring允许你封装你的O/R Mapping代码,这可以通过声明式的AOP方法拦截器或者在Java代码级别上使用一个外部的template包装类。无论使用哪一种方式,事务控制都会帮助你做相关处理,例如万一有异常发生时的事务操作(rollback)。正如我们下面要讨论的一样,你能够使用和替换各种事务管理器,却不会使你的Hibernate/JDO相关的代码受到影响。例如,不管采用本地事务还是JTA,完整的Service层的代码(如声明式事务管理)在这种场景下都是相同的。作为一个附加的功能,JDBC相关的代码能够在事务级别上与你所使用的O/R映射代码无缝整合。这一功能对于那些诸如批量处理、BLOB的操作等并不适合采用O/R Mapping操作的,但是需要与O/R Mapping操作一起参与相同的事务来说是相当有用的。

  • 避免绑定特定技术允许mix-and-match的实现策略。 虽然Hibernate非常强大、灵活、开源而且免费,但它还是使用了自己的特定的API。此外,有人也许会争辩:iBatis更轻便而且在不需要复杂的O/R映射策略的应用中使用时能够表现得非常优秀。如果可以选择的话,使用标准或抽象的API来实现主要的应用需求通常是更好的,尤其是当你可能会因为功能、性能或其他方面的原因而需要切换到另一种实现的时候。举例来说,Spring对Hibernate事务和异常抽象,允许你通过IoC机制轻松封装mapper和DAO对象来实现数据访问功能,这些特性都能够使你在 不牺牲Hibernate强大功能 的情况下在你的应用程序中隔离Hibernate的相关代码。处理DAO的高层次的service代码无需知道DAO的具体实现。这一机制可以很容易使用mix-and-match方案互不干扰地实现数据访问层(比如在一些地方用Hibernate,一些地方使用JDBC,其他地方使用iBatis), mix-and-match的特性也有利于处理遗留代码并在各种技术(JDBC、Hibernate和iBatis)之间取长补短.

在Spring发布包中的PetClinic提供了各种可选择的DAO实现和application context对JDBC、Hibernate、Oracle TopLink和JPA的配置。因而PetClinic能够作为一个Spring的web应用示例程序来描述Hibernate、TopLink和JPA的使用方法的。它同时涵盖了声明式事务中不同事务策略的配置。

JPetStore示例主要举例说明了iBATIS SQL Map在Spring环境中的使用。它同时包含了两套不同的Web层选择,一套基于Spring Web MVC,而另外一套则基于Struts。

除了Spring自身提供的示例之外,有很多其他的基于Spring的O/R Mapping的示例,他们由各自的供应商提供。例如:JDO的JPOX实现(http://www.jpox.org/)以及Kodo(http://www.bea.com/kodo)。

12.2. Hibernate

我们将首先从Hibernate (http://www.hibernate.org/)开始,通过讲解Hibernate在Spring环境中的使用来阐述Spring框架对于O/R Mapping工具的整合方式。本章节将涉及到许多细节问题,并向你展示各种不同的DAO实现方式和事务划分。这其中的绝大多数模式能够被Spring支持的其他O/R Mapping工具所使用。这一章节的其他部分将为你讲述其他的O/R Mapping工具,并给出一些简短的例子。

下面的讨论将主要集中在Hibernate 3:这也是目前最新版本的Hibernate产品。对于Spring已经支持的Hibernate 2.x也将被继续支持。在下面的例子中都使用了Hibernate 3的类与配置。而所有这些示例(绝大多数)都对Hibernate 2.x依然有效,只要你使用相应的Hibernate 2.x 支持包: org.springframework.orm.hibernate。在这个包中,有类似org.springframework.orm.hibernate3包中对应于Hibernate 2.x的支持。除此之外,为了遵循Hibernate 3在包结构上的改变,所有使用 org.hibernate 作为包结构的类实例需要替换成 net.sf.hibernate 从而适应那些包结构的改变(正如在示例中的一样)。

12.2.1. 资源管理

典型的业务程序经常会被重复的资源管理代码搞得混乱。很多项目都试图创建自己的方案来解决这个问题,有时会为了编程方便而牺牲恰当的错误处理。对于恰当的资源管理,Spring提倡一种瞩目而又简洁的解决方案:使用模板化的IoC,诸如基础构建类、回调接口以及使用AOP拦截器。基础构建类负责恰当的资源处理,以及将特定的异常代码转换为unchecked exception体系。Spring引进了DAO异常体系,可适用于任何数据访问策略。对于直接使用JDBC的情况,前面章节提到的 JdbcTemplate 类负责处理connection,并正确地把 SQLException 变为 DataAccessException 体系,包括将与数据库相关的SQL错误代码变成有意义的异常类。 Spring同时通过他们各自的事务管理器支持JTA和JDBC事务。

Spring同样也提供了对Hibernate和JDO的支持,包括 HibernateTemplate / JdoTemplate 类似于 JdbcTemplateHibernateInterceptor / JdoInterceptor 以及一个 Hibernate / JDO 事务管理器。这样做的目的是为了能够清晰地划分应用程序层次而不管使用何种数据访问和事务管理技术,从而降低各个应用程序对象之间的耦合。业务逻辑不再依赖于特定的数据访问与事务策略;不再有硬编码的资源查找、不再有难以替换的singletons、不再有用户自定义的服务注册。 Spring提供了一个简单且稳固的方案使得各种应用逻辑对象连接在一起,使这些对象可重用,并尽可能不依赖容器。所有的数据访问技术都能独立使用,但是他们在Spring提供的基于XML配置且无需依赖Spring的普通JavaBean下会与application Context整合的更好。在典型的Spring应用程序中,很多重要的对象都是JavaBeans:数据访问template、数据访问对象(使用template)、事务管理器、业务逻辑对象(使用数据访问对象和事务管理器)、web视图解析器、web控制器(使用业务对象)等等。

12.2.2. 在Spring的application context中创建 SessionFactory

为了避免硬编码的资源查找与应用程序对象紧密耦合,Spring允许你在application context中以bean的方式定义诸如JDBC DataSource或者Hibernate SessionFactory 的数据访问资源。任何需要进行资源访问的应用程序对象只需要持有这些事先定义好的实例的引用(DAO定义在下一章节介绍),下面的代码演示如何创建一个JDBC DataSource 和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.hibernate3.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.MySQLDialect
      </value>
    </property>
  </bean>

   ...
</beans>

将一个本地定义的,如Jakarta Commons DBCP的 BasicDataSource 切换为一个JNDI定位的DataSource(通常由J2EE Server管理),仅仅需要改变配置:

<beans>

  <bean id="myDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/myds"/>
  </bean>

  ...
</beans>

你也可以访问一个JNDI定位的Hibernate SessionFactory,通过使用Spring的 JndiObjectFactoryBean 来暴露和获取。 当然,如果在EJB上下文之外,这是不必要的。

12.2.3.  HibernateTemplate

对于特定的数据访问对象或业务对象的方法来说,基本的模板编程模型看起来像下面所示的代码那样。 对于这些外部对象来说,没有任何实现特定接口的要求,仅仅要求提供一个Hibernate SessionFactory。 它可以从任何地方得到,不过比较适宜的方法是从Spring的application context中得到的bean引用:通过简单的 setSessionFactory(..) 这个bean的setter方法。 下面的代码展示了在application context中一个DAO的定义,它引用了上面定义的 SessionFactory,同时展示了一个DAO方法的具体实现。

<beans>
  ...

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

</beans>
public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

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

    public Collection loadProductsByCategory(final String category) throws DataAccessException {
        HibernateTemplate ht = new HibernateTemplate(this.sessionFactory);
        return (Collection) ht.execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException {
                Query query = session.createQuery(
                    "from test.Product product where product.category=?");
                query.setString(0, category);
                return query.list();
            }
        });
    }
}

一个回调实现能够有效地在任何Hibernate数据访问中使用。HibernateTemplate 会确保当前Hibernate的 Session 对象的正确打开和关闭,并直接参与到事务管理中去。 Template实例不仅是线程安全的,同时它也是可重用的。因而他们可以作为外部对象的实例变量而被持有。对于那些简单的诸如find、load、saveOrUpdate或者delete操作的调用,HibernateTemplate 提供可选择的快捷函数来替换这种回调的实现。 不仅如此,Spring还提供了一个简便的 HibernateDaoSupport 基类,这个类提供了 setSessionFactory(..) 方法来接受一个 SessionFactory 对象,同时提供了 getSessionFactory()getHibernateTemplate() 方法给子类使用。 综合了这些,对于那些典型的业务需求,就有了一个非常简单的DAO实现:

public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        return getHibernateTemplate().find(
            "from test.Product product where product.category=?", category);
    }
}

12.2.4. 不使用回调的基于Spring的DAO实现

作为不使用Spring的 HibernateTemplate 来实现DAO的替代解决方案,你依然可以用传统的编程风格来编写你的数据访问代码。 无需将你的Hibernate访问代码包装在一个回调中,只需符合Spring的通用的 DataAccessException 异常体系。 Spring的 HibernateDaoSupport 基类提供了访问与当前事务绑定的 Session 对象的函数,因而能保证在这种情况下异常的正确转化。 类似的函数同样可以在 SessionFactoryUtils 类中找到,但他们以静态方法的形式出现。 值得注意的是,通常将一个false作为参数(表示是否允许创建)传递到 getSession(..) 方法中进行调用。 此时,整个调用将在同一个事务内完成(它的整个生命周期由事务控制,避免了关闭返回的 Session 的需要)。

public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category)
            throws DataAccessException, MyException {

        Session session = getSession(getSessionFactory(), false);
        try {
            List result = session.find(
                "from test.Product product where product.category=?",
                category, Hibernate.STRING);
            if (result == null) {
                throw new MyException("invalid search result");
            }
            return result;
        }
        catch (HibernateException ex) {
            throw convertHibernateAccessException(ex);
        }
    }
}

这种直接使用Hibernate访问代码的主要好处在于它允许你在数据访问代码中抛出checked exception,而 HibernateTemplate 却受限于回调中的unchecked exception。 注意,你通常可以将这些应用程序的异常处理推迟到回调函数之后,这样,你依然可以正常使用 HibernateTemplate。 一般来说,HibernateTemplate 所提供的许多方法在许多情况下看上去更简单和便捷。

12.2.5. 基于Hibernate3的原生API实现DAO

Hibernate 3.0.1引入了一个新的特性:“带上下文环境的Session”。 这一特性使得Hibernate自身具备了每个事务绑定当前 Session 对象的功能。 这与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();
    }
}

这种Hibernate数据访问的风格与你在Hibernate文档和示例中见到的非常类似,除了DAO实现类中持有了一个 SessionFactory 的实例变量。 我们强烈推荐这种基于实例变量的DAO构建方式,而不是使用那种过去由Hibernate的示例程序中提到的 静态的 HibernateUtil 类。 (通常来说,不要在静态变量中保存任何资源信息除非 确实 有这个必要)。

上面我们所列出的DAO完全遵循IoC:它如同使用Spring的 HibernateTemplate 进行编程那样,适合在application context中进行配置。 具体来说,它使用了Setter注入;如果你愿意,完全可以使用Constructor注入方式替代。当然,这样的DAO还可以建立在一个普通的Java类中(诸如在Unit Test中): 仅仅需要初始化这个类, 调用 setSessionFactory(..) 方法设置你所期望的工厂资源。 以Spring的bean的定义方式,它看上去就像这样:

<beans>
  ...

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

</beans>

这种DAO访问方式的主要优势在于它仅仅依赖于Hibernate API本身而无需引入任何Spring的类。 从无入侵性的角度来看,这一点非常吸引人。同时,对于Hibernate开发人员来说也更自然。

然而,这样的DAO访问方式会抛出 HibernateException,这是一个无需声明或捕获的unchecked exception。 这意味着,DAO的调用者只能以普通的错误来处理这些异常,除非完全依赖Hibernate自身的异常体系。 因而,除非你将DAO的调用者绑定到具体的实现策略上去,否则你将无法捕获特定的异常原因(诸如乐观锁异常)。 这种折中平衡或许可以被接受,如果你的应用完全基于Hibernate或者无需进行特殊的异常处理。

幸运的是,Spring的 LocalSessionFactoryBean 可以在任何Spring的事务管理策略下, 提供对Hibernate的 SessionFactory.getCurrentSession() 函数的支持,它将返回当前受Spring事务管理(甚至是 HibernateTransactionManager 管理的)的 Session 对象。 当然,该函数的标准行为依然有效:返回当前与正在进行的JTA事务(无论是Spring的 JtaTransactionManager、EJB CMT或者普通的JTA)绑定的 Session 对象。

总体来说,DAO可以基于Hibernate3的原生API实现,同时,它依旧需要能够参与到Spring的事务管理中。 这对于那些已经对Hibernate非常熟悉的人来说很有吸引力,因为这种方式更加自然。 不过,此时的DAO将抛出HibernateException,因而,如果有必要的话,需要明确地去做由 HibernateException 到Spring的 DataAccessException 的转化。

12.2.6. 编程式的事务划分

我们可以在这些低层次的数据访问服务之上的应用程序进行更高层次的事务划分,从而让事务能够横跨多个操作。 这里对于相关的业务逻辑对象同样没有实现接口的限制,它只需要一个Spring的 PlatformTransactionManager。 同SessionFactory一样,它可以来自任何地方,但是最好是一个经由 setTransactionManager(..) 方法注入的bean的引用,正如使用 setProductDao 方法来设置 productDAO 一样。 下面演示了在Spring的application context中定义一个事务管理器和一个业务对象,以及具体的业务方法的实现:

<beans>
  ...

  <bean id="myTxManager" class="org.springframework.orm.hibernate3.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 PlatformTransactionManager transactionManager;
    private ProductDao productDao;

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

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

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

12.2.7. 声明式的事务划分

作为可选方案,我们可以使用Spring声明式的事务支持。声明式的事务支持通过配置Spring容器中的AOP Transaction Interceptor来替换事务划分的硬编码。 这将使你可以从每个业务方法中重复的事务划分代码中解放出来,真正专注于为你的应用添加有价值的业务逻辑代码。此外,类似传播行为和隔离级别等事务语义能够在配置文件中改变,而不会影响具体的业务对象的实现。

<beans>
    ...

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

  <bean id="myProductService" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="product.ProductService"/>
    <property name="target">
        <bean class="product.DefaultProductService">
            <property name="productDao" ref="myProductDao"/>
        </bean>
    </property>
    <property name="interceptorNames">
      <list>
        <value>myTxInterceptor</value> <!-- the transaction interceptor (configured elsewhere) -->
      </list>
    </property>
  </bean>

</beans>
public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

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

    // notice the absence of transaction demarcation code in this method 
				// Spring's declarative transaction infrastructure will be demarcating transactions on your behalf 
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDAO.loadProductsByCategory(category);
        ...
    }

    ...
}

Spring的 TransactionInterceptor 允许任何应用程序的checked exception在回调代码中抛出,而 TransactionTemplate 在回调中则严格要求被封装成unchecked exception。 TransactionTemplate 会在一个unchecked exception抛出时,或者当事务被应用程序通过 TransactionStatus 标记为rollback-only时触发一个数据库回滚操作。 TransactionInterceptor 默认进行同样的操作,但是它允许对每个方法配置自己的rollback策略。

下面列出的高级别的声明式的事务管理方案并没有使用 ProxyFactoryBean,它将使那些大量的业务对象需要被纳入到事务管理中时的配置变得非常简单。

Note

在你打算继续阅读下一部分之前,我们强烈推荐你想去阅读 Section 9.5, “声明式事务管理” 这一章节。

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

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

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

  <aop:config>
    <aop:pointcut id="productServiceMethods" expression="execution(* product.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
  </aop:config>

  <tx:advice id="txAdvice" transaction-manager="myTxManager">
    <tx:attributes>
      <tx:method name="increasePrice*" propagation="REQUIRED"/>
      <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
      <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
  </tx:advice>

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

</beans>

12.2.8. 事务管理策略

TransactionTemplateTransactionInterceptor 都将真正的事务处理委托给一个 PlatformTransactionManager 实例来处理。在Hibernate应用中,它可以是一个 HibernateTransactionManager(对于单独一个的Hibernate SessionFactory 使用一个 ThreadLocalSession)或一个 JtaTransactionManager(代理给容器的JTA子系统)。你甚至可以使用自定义的 PlatformTransactionManager 的实现。因而,在你的应用碰到了特定的分布式事务的部署需求时(类似将原来的Hibernate事务管理转变为JTA),仅仅需要改变配置而已:简单将Hibernate的事务管理器替换成JTA事务实现。任何的事务划分和数据访问的代码都无需因此而改变,因为他们仅仅使用了通用的事务管理API。

对于横跨多个Hibernate SessionFacotry的分布式事务,只需简单地将 JtaTransactionManager 同多个 LocalSessionFactoryBean 的定义结合起来作为事务策略。你的每一个DAO通过bean属性得到各自的 SessionFactory 引用。如果所有的底层JDBC数据源都是支持事务的容器,那么只要业务对象使用了 JtaTransactionManager 作为事务策略,它就可以横跨多个DAO和多个session factories来划分事务,而不需要做任何特殊处理。

<beans>

  <bean id="myDataSource1" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName value="java:comp/env/jdbc/myds1"/>
  </bean>

  <bean id="myDataSource2" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/myds2"/>
  </bean>

  <bean id="mySessionFactory1" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource1"/>
    <property name="mappingResources">
      <list>
        <value>product.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.MySQLDialect
        hibernate.show_sql=true
      </value>
    </property>
  </bean>

  <bean id="mySessionFactory2" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource2"/>
    <property name="mappingResources">
      <list>
        <value>inventory.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.OracleDialect
      </value>
    </property>
  </bean>

  <bean id="myTxManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

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

  <bean id="myInventoryDao" class="product.InventoryDaoImpl">
    <property name="sessionFactory" ref="mySessionFactory2"/>
  </bean>

  <!-- this shows the Spring 1.x style of declarative transaction configuration -->
  <!-- it is totally supported, 100% legal in Spring 2.x, but see also above for the sleeker, Spring 2.0 style -->
  <bean id="myProductService"
      class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="myTxManager"/>
    <property name="target">
      <bean class="product.ProductServiceImpl">
        <property name="productDao" ref="myProductDao"/>
        <property name="inventoryDao" ref="myInventoryDao"/>
      </bean>
    </property>
    <property name="transactionAttributes">
      <props>
        <prop key="increasePrice*">PROPAGATION_REQUIRED</prop>
        <prop key="someOtherBusinessMethod">PROPAGATION_REQUIRES_NEW</prop>
        <prop key="*">PROPAGATION_SUPPORTS,readOnly</prop>
      </props>
    </property>
  </bean>

</beans>

HibernateTransactionManagerJtaTransactionManager 都允许恰当的JVM级别的Hibernate缓存处理,而无需使用与容器相关的事务管理器或JCA连接器。(只要不是由EJB发起的事务)

对于特定的 DataSourceHibernateTransactionManager 能够为普通的JDBC访问代码提供Hibernate所使用的 Connection。 这一功能允许你可以完全混和使用Hibernate/JDBC进行数据访问而无需依赖JTA在高层次代码中进行事务划分,只要你依然访问的是同一个数据库! HibernateTransactionManager 能够自动地将Hibernate事务暴露为JDBC事务,如果你通过设置 DataSource来建立SessionFactory 对象(通过设置 LocalSessionFactoryBean 中的“dataSource”属性)。 另外一种配置方法是通过设置 HibernateTransactionManager 的“dataSource”属性,明确指定事务需要暴露的 DataSource

12.2.9. 容器资源 vs 本地资源

Spring的资源管理允许你简单地在一个JNDI的 SessionFactory 和一个本地的 SessionFactory 之间切换而无需更改任何一行应用程序代码。 把资源定义放在容器中还是放在应用程序本地中主要是由使用的事务策略决定的。与Spring定义的本地 SessionFactory 相比,一个手工注册的JNDI SessionFactory 并没有体现出多大的优势。 通过Hibernate的JCA连接器来部署一个 SessionFactory 提供了能使之参与到J2EE服务器管理架构的增值服务,不过除此之外也并没有增加实际的价值。

Spring对事务管理的支持有一个非常重要的好处:它不依赖于任何容器。使用非JTA的任何其他事务策略的配置,程序也能在独立的测试环境下正常工作。 尤其对于那些典型的单数据库事务情况下,这将是一个非常轻量级而又强大的JTA替代方案。当你使用一个本地的EJB SLSB来驱动事务时,即时你可能只访问一个数据库而且仅仅通过CMT使用SLSB的声明性事务,你仍然要依赖于EJB容器和JTA。 使用编程式JTA替换方案依然需要J2EE环境,JTA不仅对于JTA本身,还对于JNDI的 DataSource 实例引入了容器依赖。 对于非Spring环境的JTA驱动的Hibernate事务,你必须使用Hibernate JCA连接器或者额外的Hibernate事务代码及TransactionManagerLookup配置,来恰当地处理JVM级别的缓存。

Spring驱动的事务管理对于本地定义的 SessionFactory 能够工作得非常好,就像使用本地的JDBC DataSource 一样,当然前提必须是访问单个数据库。 因此当你真正面对分布式事务的需求时,可以马上回到Spring的JTA事务。必须要注意,一个JCA连接器需要特定容器的部署步骤,而且首先容器要支持JCA。 这要比使用本地资源定义和Spring驱动事务来部署一个简单的Web应用麻烦得多。而且你通常需要特定企业版本的容器,但是像类似WebLogic的Express版本并不提供JCA。 一个仅使用本地资源并且针对一个数据库的事务操作的Spring应用将可以在任何一种J2EE的Web容器中工作(不需要JTA、JCA或者EJB),比如Tomcat、Resin甚至普通的Jetty。 除此之外,这样的中间层可以在桌面应用或测试用例中被简单地重用。

考虑一下所有的情况:如果你不使用EJB,坚持使用本地 SessionFactory 以及Spring的 HibernateTransactionManager 或者 JtaTransactionManager, 你将获得包括适当的JVM级别上的缓存以及分布式事务在内的所有好处,而不会有任何容器部署的麻烦。通过JCA连接器的Hibernate的 SessionFactory 的JNDI注册仅仅在EJB中使用会带来好处。

12.2.10. 在应用服务器中使用Hibernate的注意点

在一些具有非常严格的XADataSource实现的JTA环境中(目前来说只是WebLogic和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同步)。可以有两种方法达到这种效果:

  • 如果你已经在你的application context中定义并获取了JTA PlatformTransactionManager 对象(或许来自通过 JndiObjectFactoryBean 得到的JNDI)并已经将它注入到类似Spring的 JtaTransactionManager 中, 那么最简单的方式就是指定这个bean的引用作为 LocalSessionFactoryBeanjtaTransactionManager 属性。Spring将使这个对象被Hibernate所感知。

  • 多数情况下,你还没有得到JTA的 PlatformTransactionManager 实例(由于Spring的 JtaTransactionManager 能够自己找到它),所以你需要自行配置Hibernate并直接寻找资源。 正如Hibernate文档中提到的,这可以通过在Hibernate配置一个应用服务器特定的 TransactionManagerLookup 类来完成。

对于恰当的使用方法,你无需了解更多的东西。但是我们在这里将描述一下,对于将Hibernate配置为感知或者不感知JTA的 PlatformTransactionManager 对象这两种情况下,整个事务的执行顺序。

Hibernate被配置成不感知JTA PlatformTransactionManager 的情况下,当一个JTA事务提交时,整个事件的执行顺序如下:

  • JTA 事务提交

  • Spring的 JtaTransactionManager 同步到JTA事务,它被JTA事务管理器通过调用 afterCompletion 执行回调。

  • 在所有其他活动中,这将由Spring向Hibernate触发一个回调,通过Hibernate的 afterTransactionCompletion 回调(用于清除Hibernate缓存),然后紧跟着一个明确的Hibernate Session的 close() 调用。 这将使Hibernate试图去关闭JDBC的Connection。

  • 在某些环境中,Connection.close() 的调用将会触发一个警告或者错误信息。 这是由于特定的应用服务器将不再考虑Connection的可用性,因为此时事务已经被提交了。

Hibernate被配置成感知JTA的 PlatformTransactionManager 的情况下,当一个JTA事务提交时,整个事件的执行顺序如下:

  • JTA 事务准备提交

  • Spring的 JtaTransactionManager 同步到JTA事务,因而它被JTA事务管理器通过调用 beforeCompletion 执行回调。

  • Spring能感知到Hibernate自身被同步到JTA Transaction,因而表现得与先前那种情况有所不同。 假设Hibernate的 Session 需要被关闭,Spring将负责关闭它。

  • JTA 事务提交

  • Hibernate将与JTA transaction进行同步,因而它被JTA事务管理器通过调用 afterCompletion 执行回调,并且适时地清除缓存。

 

12.3. JDO

Spring支持标准的JDO 1.0/2.0 API作为数据访问策略,同样遵循类似于Spring对Hibernate的支持风格。对应的支持与整合类位于 org.springframework.orm.jdo 包中。

12.3.1. 建立PersistenceManagerFactory

Spring提供了一个 LocalPersistenceManagerFactoryBean 类,允许你通过Spring的application context来定义一个本地的JDO PersistenceManagerFactory

<beans>

  <bean id="myPmf" class="org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean">
    <property name="configLocation" value="classpath:kodo.properties"/>
  </bean>

  ...
</beans>

作为可选方案,PersistenceManagerFactory 也可以通过直接实例化一个 PersistenceManagerFactory 的实现类得到。 一个JDO PersistenceManagerFactory 的实现类遵循JavaBeans的模式,它非常像一个JDBC DataSource 的实现类,这使得它天然的非常适合作为一个Spring的bean定义。 这种创建风格通常支持一个Spring定义的JDBC DataSource,将它传入到对应的实现类的connectionFactory的属性中进行bean的创建。具体的例子参见开源的JDO实现JPOX(http://www.jpox.org):

<beans>

 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
   <property name="driverClassName" value="${jdbc.driverClassName}"/>
   <property name="url" value="${jdbc.url}"/>
   <property name="username" value="${jdbc.username}"/>
   <property name="password" value="${jdbc.password}"/>
 </bean>

 <bean id="myPmf" class="org.jpox.PersistenceManagerFactoryImpl" destroy-method="close">
   <property name="connectionFactory" ref="dataSource"/>
   <property name="nontransactionalRead" value="true"/>
 </bean>

 ...
</beans>

一个JDO的 PersistenceManagerFactory 能够同样在一个J2EE应用服务器的JNDI环境下被创建。 这通常由特定的JDO实现所提供的JCA连接器来完成。Spring标准的 JndiObjectFactoryBean 也能够被用来获取和暴露这个 PersistenceManagerFactory。 当然,通常在一个EJB环境之外,在JNDI中持有 PersistenceManagerFactory 的实例没有什么特别吸引人的好处,因而我们一般只在有非常充足的理由时选择这种建立方式。 请参看Hibernate章节中“容器资源 vs 本地资源”这一节的讨论,那里的讨论同样适用于JDO。

12.3.2.  JdoTemplateJdoDaoSupport

每一个基于JDO的DAO类都需要通过IoC来注入一个 PersistenceManagerFactory,你可以通过Setter方式注入,也可以用构造函数方式注入。 这样的DAO类可以在 PersistenceManagerFactory 的帮助下来操作原生的JDO API进行编程,但是通常来说我们更愿意使用Spring提供的 JdoTemplate

<beans>
  ...

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmf"/>
  </bean>

</beans>
public class ProductDaoImpl implements ProductDao {

    private PersistenceManagerFactory persistenceManagerFactory;

    public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
        this.persistenceManagerFactory = pmf;
    }

    public Collection loadProductsByCategory(final String category) throws DataAccessException {
        JdoTemplate jdoTemplate = new JdoTemplate(this.persistenceManagerFactory);
        return (Collection) jdoTemplate.execute(new JdoCallback() {
            public Object doInJdo(PersistenceManager pm) throws JDOException {
                Query query = pm.newQuery(Product.class, "category = pCategory");
                query.declareParameters("String pCategory");
                List result = query.execute(category);
                // do some further stuff with the result list
                return result;
            }
        });
    }
}

一个回调的实现能够有效地在任何JDO数据访问中使用。JdoTemplate 会确保当前的 PersistenceManager 对象的正确打开和关闭,并自动参与到事务管理中去。 Template实例不仅是线程安全的,同时它也是可重用的,因而他们可以作为外部对象的实例变量而被持有。对于那些简单的诸如 findloadmakePersistent 或者 delete 操作的调用, JdoTemplate 提供可选择的快捷函数来替换这种回调的实现。 不仅如此,Spring还提供了一个简便的 JdoDaoSupport 基类,这个类提供了 setPersistenceManagerFactory(..) 方法来接受一个 PersistenceManagerFactory 对象, 同时提供了 getPersistenceManagerFactory()getJdoTemplate() 给子类使用。 综合了这些,对于那些典型的业务需求,就有了一个非常简单的DAO实现。

public class ProductDaoImpl extends JdoDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        return getJdoTemplate().find(
            Product.class, "category = pCategory", "String category", new Object[] {category});
    }
}

作为不使用Spring的 JdoTemplate 来实现DAO的替代解决方案,你依然可以通过在原生JDO API的级别对那些基于Spring的DAO进行编程,此时你必须明确地打开和关闭一个 PersistenceManager。 正如在相应的Hibernate章节描述的一样,这种做法的主要优点在于你的数据访问代码可以在整个过程中抛出checked exceptions。JdoDaoSupport 为这种情况提供了多种函数支持, 包括获取和释放一个具备事务管理功能的 PersistenceManager 和相关的异常转化。

12.3.3. 基于原生的JDO API实现DAO

我们可以直接操作JDO API来实现DAO,通过直接使用一个注入的 PersistenceManagerFactory,而无需对Spring产生的任何依赖。 一个相应的DAO实现看上去就像下面这样:

public class ProductDaoImpl implements ProductDao {

    private PersistenceManagerFactory persistenceManagerFactory;

    public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
        this.persistenceManagerFactory = pmf;
    }

    public Collection loadProductsByCategory(String category) {
        PersistenceManager pm = this.persistenceManagerFactory.getPersistenceManager();
        try {
            Query query = pm.newQuery(Product.class, "category = pCategory");
            query.declareParameters("String pCategory");
            return query.execute(category);
        }
        finally {
          pm.close();
        }
    }
}

上面我们所列出的DAO完全遵循IoC:它如同使用Spring的 JdoTemplate 进行编码那样,非常适合在application context中进行配置:

<beans>
  ...

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmf"/>
  </bean>

</beans>

这类DAO的主要问题在于他们每次总是从工厂中得到一个新的 PersistenceManager 实例。 为了依然能够访问受到Spring管理的、具备事务管理功能的 PersistenceManager,不妨考虑一下在目标 PersistenceManagerFactory 之前, 定义一个 TransactionAwarePersistenceManagerFactoryProxy(Spring已经提供),然后将这个代理注入到你的DAO中去。

<beans>
  ...

  <bean id="myPmfProxy"
      class="org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy">
    <property name="targetPersistenceManagerFactory" ref="myPmf"/>
  </bean>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmfProxy"/>
  </bean>

  ...
</beans>

这样,你的数据访问代码就可以通过 PersistenceManagerFactory.getPersistenceManager() 方法得到一个具备事务管理功能的 PersistenceManager。 而这一方法将通过代理类的处理:在从工厂获取一个新的 PersistenceManager 实例之前检查一下当前具备事务管理功能的 PersistenceManager, 如果这是一个具备事务管理功能的 PersistenceManagerclose() 调用在此时将被忽略。

如果你的数据访问代码总是在一个处于活跃状态的事务中执行(或者至少在一个活跃的事务同步中),那么你能够非常安全地忽略 PersistenceManager.close() 的调用和整个 finally 块的代码。 这将使你的DAO实现变得比较简洁:

public class ProductDaoImpl implements ProductDao {

    private PersistenceManagerFactory persistenceManagerFactory;

    public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
        this.persistenceManagerFactory = pmf;
    }

    public Collection loadProductsByCategory(String category) {
        PersistenceManager pm = this.persistenceManagerFactory.getPersistenceManager();
        Query query = pm.newQuery(Product.class, "category = pCategory");
        query.declareParameters("String pCategory");
        return query.execute(category);
    }
}

对于这种依赖于活跃事务的DAO,比较推荐的做法是将 TransactionAwarePersistenceManagerFactoryProxy 中的"allowCreate"标志关闭,从而强制活跃事务。

<beans>
  ...

  <bean id="myPmfProxy"
      class="org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy">
    <property name="targetPersistenceManagerFactory" ref="myPmf"/>
    <property name="allowCreate" value="false"/>
  </bean>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="persistenceManagerFactory" ref="myPmfProxy"/>
  </bean>

  ...
</beans>

这种DAO访问方式的主要优势在于它仅仅依赖于JDO API本身而无需引入任何的Spring的类。从无入侵性的角度来看,这一点非常吸引人。同时,对于JDO开发人员来说也更自然。

然而,这样的DAO访问方式会抛出 JDOException,这是一个无需声明或捕获的unchecked exception。 这意味着,DAO的调用者只能以普通的错误来处理这些异常,除非完全依赖JDO自身的异常体系。 因而,除非你将DAO的调用者绑定到具体的实现策略上去,否则你将无法捕获特定的异常原因(诸如乐观锁异常)。 这种折中平衡或许可以被接受,如果你的应用完全基于JDO或者无需进行特殊的异常处理。

总体来说,DAO可以基于JDO的原生API实现,同时,它依旧需要能够参与到Spring的事务管理中。 这对于那些已经对JDO非常熟悉的人来说很有吸引力,因为这种方式更加自然。 不过,这种DAO将抛出 JDOException,因而,如果有必要的话需要明确地去做由 JDOException 到Spring的 DataAccessException 的转化。

12.3.4. 事务管理

将事务管理纳入到Service操作的执行中,你可以使用Spring通用的声明式的事务管理功能,参加下面的例子:

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

  <bean id="myTxManager" class="org.springframework.orm.jdo.JdoTransactionManager">
    <property name="persistenceManagerFactory" ref="myPmf"/>
  </bean>

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

  <tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="increasePrice*" propagation="REQUIRED"/>
      <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
      <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
  </tx:advice>

  <aop:config>
    <aop:pointcut id="productServiceMethods" expression="execution(* product.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
  </aop:config>

</beans>

注意,JDO要求你必须在一个活跃的事务中修改一个持久化对象。与Hibernate相比,在JDO中并没有一种类似脱离事务刷出(non-transactional flush)的概念。 基于这种原因,你所选择的JDO实现需要被建立在特定的环境中,尤其是它需要为JTA同步做明确的创建,由此来自行检测一个JTA事务。 这一点对于本地事务不是必要的,由于它已经被Spring的 JdoTransactionManager 处理,但是对于需要参与到JTA事务中的情况,是必须的(无论是由Spring的 JtaTransactionManager、EJB CMT或者普通的JTA所驱动的事务)。

JdoTransactionManager 能够将一个JDO事务暴露给访问相同的JDBC DataSource 的JDBC访问代码。前提条件是,被注册的 JdoDialect 能够支持获取底层的JDBC Connection。 这一功能默认被基于JDBC的JDO 2.0 实现。对于JDO 1.0的实现,必须注册一个用户自定义的 JdoDialect。具体参见下一节有关 JdoDialect 的机制。

12.3.5. JdoDialect

作为高级特性,JdoTemplateJdoTransactionManager 都支持一个用户自定义的 JdoDialect 作为“jdoDialect”的bean属性进行注入。 在这种情况下,DAO将不再接收 PersistenceManagerFactory 的引用作为参数,而是接收一个完整的 JdoTemplate 实例(也就是将它注入到 JdoDaoSupport 的"jdoTemplate"属性中去)。 一个 JdoDialect 实现能够激活一些由Spring支持的高级特性,这通常由特定的实现供应商指定:

  • 执行特定的事务语义(例如用户自定义的事务隔离级别和事务超时)

  • 获取具备事务功能的JDBC Connection (暴露给基于JDBC的DAO)

  • 应用查询超时功能(自动地从Spring管理的事务超时中计算)

  • 及时刷出 PersistenceManager (使得事务变化对于基于JDBC的数据访问代码可见)

  • JDOExceptions 到Spring的 DataAccessExceptions 的高级转化。

这对于JDO 1.0的实现有特别的价值,由于这些特性都没有在其标准的API中包含。 对于JDO 2.0,其中的绝大多数的特性已经以标准的方式被支持。因而,Spring的 DefaultJdoDialect 在默认情况下(Spring 1.2后)使用相应的JDO 2.0 API函数。 对于特殊的事务语义和异常的高级转化这样的高级特性,获取和使用JDO实现供应商特定的 JdoDi

分享到:
评论
pengjianbo1
  • 浏览: 108317 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

你可能感兴趣的:(spring,数据结构,Hibernate,bean,orm)