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

12.1. 简介

Spring在资源管理,DAO实现支持以及事务策略等方面提供了与 Hibernate,JDO,Oracle TopLink,Apache OJBiBATIS SQL Mapping 以及 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和Apache OJB的配置。 因而PetClinic能够作为一个Spring的web应用示例程序来描述Hibernate、TopLink和OJB的使用方法的。它同时涵盖了声明式事务中不同事务策略的配置。

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]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.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="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.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">
  ...

  <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实现供应商特定的 JdoDialect 子类还是比较有价值的。

更多有关它的操作以及它如何在Spring的JDO支持中使用的详细信息请参看 JdoDialect 的Javadoc。

12.4. Oracle TopLink

Spring从Spring 1.2版本开始支持Oracle TopLink (http://www.oracle.com/technology/products/ias/toplink)作为数据访问策略,同样遵循类似于Spring对Hibernate的支持风格。 Spring对其的支持包括TopLink 9.0.4(Spring 1.2支持的产品版本)和TopLink 10.1.3(Spring 1.2支持的,依然处于beta版)。 对应的支持与整合类位于 org.springframework.orm.toplink 包中。

Spring的TopLink支持已经和Oracle TopLink团队共同开发。非常感谢TopLink团队,尤其是Jim Clark,帮助我们澄清了所有方面的具体事项!

12.4.1.  SessionFactory 抽象层

TopLink本身并不运行在SessionFactory抽象层上,多线程的数据访问是建立在中央 ServerSession 上的。 每当一个请求轮询到被处理的时候,这个中央 ServerSession 会为单个线程产生一个 ClientSession 的实例供其使用。 为了提供灵活便捷的创建选项,Spring为TopLink定义了一个 SessionFactory 接口,从而使你可以任意地在不同的 Session 创建策略之间进行切换。

作为一个一站式的百货商店,Spring提供了一个 LocalSessionFactoryBean 类,允许你以bean风格的配置方式来定义一个TopLink的 SessionFactory。 需要进行配置的地方主要是TopLink session配置文件的位置,通常来说还需配置一个受到Spring管理的JDBC DataSource

<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="mySessionFactory" class="org.springframework.orm.toplink.LocalSessionFactoryBean">
    <property name="configLocation" value="toplink-sessions.xml"/>
    <property name="dataSource" ref="dataSource"/>
  </bean>

  ...
</beans>
<toplink-configuration>

  <session>
    <name>Session</name>
    <project-xml>toplink-mappings.xml</project-xml>
    <session-type>
      <server-session/>
    </session-type>
    <enable-logging>true</enable-logging>
    <logging-options/>
  </session>

</toplink-configuration>

通常情况下,LocalSessionFactoryBean 在底层将持有一个多线程的TopLink ServerSession 并创建合适的客户端 Session: 它或者是一个普通的 Session(典型情况) —— 一个受管理的 ClientSession;或者是一个具备事务功能的 Session (后者主要在Spring内部对TopLink的支持中被使用)。 还有一种情况,LocalSessionFactoryBean 可能会持有一个单线程的TopLink的 DatabaseSession,这是非常特殊的情况了。

12.4.2.  TopLinkTemplateTopLinkDaoSupport

每个基于TopLink的DAO将通过IoC被注入一个 SessionFactory,你可以通过Setter方式注入,也可以用构造函数方式注入。 这样的DAO可以直接操作原生的TopLink API,通过 SessionFactory 来获取一个 Session,但是通常情况下,你更愿意使用Spring的 TopLinkTemplate

<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 {
        TopLinkTemplate tlTemplate = new TopLinkTemplate(this.sessionFactory);
        return (Collection) tlTemplate.execute(new TopLinkCallback() {
            public Object doInTopLink(Session session) throws TopLinkException {
                ReadAllQuery findOwnersQuery = new ReadAllQuery(Product.class);
                findOwnersQuery.addArgument("Category");
                ExpressionBuilder builder = this.findOwnersQuery.getExpressionBuilder();
                findOwnersQuery.setSelectionCriteria(
                    builder.get("category").like(builder.getParameter("Category")));

                Vector args = new Vector();
                args.add(category);
                List result = session.executeQuery(findOwnersQuery, args);
                // do some further stuff with the result list
                return result;
            }
        });
    }
}

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

public class ProductDaoImpl extends TopLinkDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        ReadAllQuery findOwnersQuery = new ReadAllQuery(Product.class);
        findOwnersQuery.addArgument("Category");
        ExpressionBuilder builder = this.findOwnersQuery.getExpressionBuilder();
        findOwnersQuery.setSelectionCriteria(
            builder.get("category").like(builder.getParameter("Category")));

        return getTopLinkTemplate().executeQuery(findOwnersQuery, new Object[] {category});
    }
}</programlisting>

边注:TopLink查询对象是线程安全的,并且能够在DAO层被缓存。在一开始被创建时以实例变量的方式被保持。

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

12.4.3. 基于原生的TopLink API的DAO实现

我们可以直接操作TopLink API来实现DAO,直接使用一个注入的 Session 而无需对Spring产生的任何依赖。 它通常基于一个由 LocalSessionFactoryBean 定义的 SessionFactory,并通过Spring的 TransactionAwareSessionAdapter 暴露成为一个 Session 类型的引用。

TopLink的 Session 接口中定义的 getActiveSession() 方法将返回当前具备事务管理功能的 Session 对象。 如果当前没有处于活跃状态的事务,这个函数将返回一个共享的TopLink的 ServerSession,也就是说,这种情况应该只是一个直接使用的只读访问。 另外还有一个 getActiveUnitOfWork() 方法, 返回TopLink的与当前事务绑定的 UnitOfWork (如果没有当前事务则返回null)。

一个相应的DAO实现类看上去就像下面那样:

public class ProductDaoImpl implements ProductDao {

    private Session session;

    public void setSession(Session session) {
        this.session = session;
    }

    public Collection loadProductsByCategory(String category) {
        ReadAllQuery findOwnersQuery = new ReadAllQuery(Product.class);
        findOwnersQuery.addArgument("Category");
        ExpressionBuilder builder = this.findOwnersQuery.getExpressionBuilder();
        findOwnersQuery.setSelectionCriteria(
            builder.get("category").like(builder.getParameter("Category")));

        Vector args = new Vector();
        args.add(category);
        return session.getActiveSession().executeQuery(findOwnersQuery, args);
    }
}

上面我们所列出的DAO完全遵循IoC:它如同使用Spring的 TopLinkTemplate 进行编码那样,非常适合在application context中进行配置。 Spring的 TransactionAwareSessionAdapter 将暴露一个 Session 类型的bean的引用,并传入到DAO中去:

<beans>
  ...

  <bean id="mySessionAdapter"
      class="org.springframework.orm.toplink.support.TransactionAwareSessionAdapter">
    <property name="sessionFactory" ref="mySessionFactory"/>
  </bean>

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="session" ref="mySessionAdapter"/>
  </bean>

  ...
</beans>

这种DAO风格的主要好处在于它仅仅依赖于TopLink自身的API,而无需引入任何的Spring的类。从无入侵性的角度来看,这一点非常吸引人。同时,对于TopLink的开发人员来说也更自然。

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

这样的DAO风格有一个不利因素在于TopLink的标准的 getActiveSession() 函数仅仅在JTA事务中有效。而对于其他的事务管理策略尤其时本地的TopLink事务,它将无法工作。

幸运的是,Spring的 TransactionAwareSessionAdapter 为TopLink的 ServerSession 暴露了一个相应的代理类。 这个代理类能够在任何的事务策略之上支持TopLink的 Session.getActiveSession()Session.getActiveUnitOfWork() 函数, 返回当前收到Spring管理(即便由 TopLinkTransactionManager 管理)的具备事务管理功能的 Session 实例。 当然,这个函数的标准行为依然有效:返回与当前的JTA事务绑定的 Session 对象。(无论这个JTA事务是由Spring的 JtaTransactionManager、EJB CMT或者普通的JTA所驱动的事务)。

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

12.4.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.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">
  ...

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

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </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>

</beans>

注意,TopLink要求你必须在一个活跃的 工作单元(UnitOfWork) 中修改一个持久化对象(你通常不能修改由普通的TopLink的 Session 查询返回的对象,因为这些对象通常是一些从二级缓存中读出的只读对象)。 与Hibernate相比,在TopLink中并没有一种类似脱离事务刷出(non-transactional flush)的概念。 基于这种原因,TopLink需要被建立在特定的环境中,尤其是它需要为JTA同步做明确的创建,由此来自行检测一个JTA事务以及暴露一个相应的活跃的 SessionUnitOfWork。 这一点对于本地事务不是必要的,由于它已经被Spring的 TopLinkTransactionManager 处理,但是对于需要参与到JTA事务中的情况,是必须的(无论是由Spring的 JtaTransactionManager、EJB CMT或者普通的JTA所驱动的事务)。

在你的基于TopLink的DAO代码中,你可以使用 Session.getActiveUnitOfWork() 方法来访问当前的 UnitOfWork 并通过它来执行写操作。 这将只在一个活跃的事务中有效(在一个收到Spring管理的事务或者JTA事务中)。对于特殊的需求,你同样可以获取单独的 UnitOfWork 实例,它将不参与到当前的事务中去,不过这种情况非常少。

TopLinkTransactionManager 能够将一个TopLink事务暴露给访问相同的JDBC DataSource 的JDBC访问代码。 前提条件是,TopLink在底层是以JDBC方式工作的并且能够暴露底层的JDBC Connection。 这种情况下,用于暴露事务的 DataSource 必须被明确指定,它是无法被自动检测到的。

12.5. Apache OJB

Apache OJB (http://db.apache.org/ojb) 提供了不同的API级别,例如ODMG和JDO。 除了通过JDO支持OJB,Spring同样为OJB的低层次的 PersistenceBroker API作为数据访问提供支持。 相应的整合与支持的类位于 org.springframework.orm.ojb 包中。

12.5.1. 在Spring环境中建立OJB

与Hibernate或者JDO相比,OJB并没有使用工厂模式进行资源管理,PersistenceBroker 必须通过从一个静态的 PersistenceBrokerFactory 类中获得。 这个工厂类根据一个位于classpath目录下的OJB.properties配置文件进行初始化。

除了对OJB的默认的初始化方式进行支持,Spring还提供了一个 LocalOjbConfigurer 的类。 它允许使用受到Spring管理的 DataSource 实例作为OJB链接的提供者。 DataSource 实例则在OJB资源描述器(配置文件)中进行描述,通过"jcd-alias"的定义完成:每个别名都与受Spring管理的同名的bean匹配。

<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="ojbConfigurer" class="org.springframework.orm.ojb.support.LocalOjbConfigurer"/>

  ...
</beans>
<descriptor-repository version="1.0">

  <jdbc-connection-descriptor jcd-alias="dataSource" default-connection="true" ...>
        ...
  </jdbc-connection-descriptor>

  ...
</descriptor-repository>

一个 PersistenceBroker 此时可以通过标准的OJB的API打开,并指定一个对应的“PBKey”。 这通常通过对应的"jcd-alias"定义来完成(或者依赖于默认的连接)。

12.5.2.  PersistenceBrokerTemplatePersistenceBrokerDaoSupport

每个基于OJB的DAO都将通过一个"PBKey"以bean方式进行配置,配置通过Setter注入方式完成。这样的DAO能够通过OJB的静态的 PersistenceBrokerFactory 类直接操作普通的OJB API进行编程。 当然你可以更愿意使用Spring的 PersistenceBrokerTemplate

<beans>
  ...

  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="jcdAlias" value="dataSource"/> <!-- can be omitted (default) -->
  </bean>

</beans>
public class ProductDaoImpl implements ProductDao {

    private String jcdAlias;

    public void setJcdAlias(String jcdAlias) {
        this.jcdAlias = jcdAlias;
    }

    public Collection loadProductsByCategory(final String category) throws DataAccessException {
        PersistenceBrokerTemplate pbTemplate =
                new PersistenceBrokerTemplate(new PBKey(this.jcdAlias);
        return (Collection) pbTemplate.execute(new PersistenceBrokerCallback() {
            public Object doInPersistenceBroker(PersistenceBroker pb)
                    throws PersistenceBrokerException {

                Criteria criteria = new Criteria();
                criteria.addLike("category", category + "%");
                Query query = new QueryByCriteria(Product.class, criteria);

                List result = pb.getCollectionByQuery(query);
                // do some further stuff with the result list
                return result;
            }
        });
    }
}

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

public class ProductDaoImpl extends PersistenceBrokerDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        Criteria criteria = new Criteria();
        criteria.addLike("category", category + "%");
        Query query = new QueryByCriteria(Product.class, criteria);

        return getPersistenceBrokerTemplate().getCollectionByQuery(query);
    }
}

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

12.5.3. 事务管理

将事务管理纳入到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.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">
  ...

  <bean id="myTxManager" class="org.springframework.orm.ojb.PersistenceBrokerTransactionManager">
    <property name="jcdAlias" value="dataSource"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </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>

</beans>

注意OJB的 PersistenceBroker 级别的API并不跟踪加载的对象的变化。 因而,一个 PersistenceBroker 的事务非常有必要成为一个简单的在 PersistenceBroker 级别的事务而仅仅加入一个为持久对象而设置的一级缓存。 此时,懒式加载无论在 PersistenceBroker 打开或者关闭时都将工作得很好。这一点与Hibernate和JDO相比有很大得不同。(懒式加载仅仅对于 Session 或者 PersistenceManager 各自保持打开时才有效)。

PersistenceBrokerTransactionManager 能够将一个OJB事务暴露给访问相同的JDBC DataSource 的JDBC访问代码。 前提条件是,所需要暴露事务的 DataSource 必须被明确指定,它无法被自动检测到。

12.6. iBATIS SQL Maps

Spring通过 org.springframework.orm.ibatis 包来支持iBATIS SQL Maps 1.x和2.x (http://www.ibatis.com)。 与JDBC/Hibernate支持非常类似,Spring对于iBATIS的支持也采用了Template的风格,同样遵循Spring的异常体系,这些会让你喜欢上Spring的所有IoC特性。

事务管理可以由Spring标准机制进行处理。对于iBATIS来说没有特别的事务策略,除了JDBC Connection 以外,也没有特别的事务资源。 因此,Spring标准的JDBC DataSourceTransactionManager 或者 JtaTransactionManager 已经能够完全足够了。

12.6.1. iBATIS 1.x和2.x的概览与区别

Spring同时支持iBATIS SQL Maps 1.x和2.x。首先让我们先来看一下两者的区别。

两者XML配置文件有一点区别,节点和属性名有了些改动。你所要继承的Spring类和方法名也有一些区别。

Table 12.1. iBATIS SQL Maps 1.x和2.x的支持类

特性1.x2.x
SqlMap(Client)的创建SqlMapFactoryBeanSqlMapClientFactoryBean
Template风格的帮助类SqlMapTemplateSqlMapClientTemplate
使用MappedStatement的回调SqlMapCallbackSqlMapClientCallback
DAO基类SqlMapDaoSupportSqlMapClientDaoSupport

12.6.2. iBATIS SQL Maps 1.x

12.6.2.1. 创建SqlMap

使用iBATIS SQL Maps包括创建一个SqlMap配置文件来定义sql语句和结果映射。Spring会通过 SqlMapFactoryBean 来加载并处理这些配置。

public class Account {

    private String name;
    private String email;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

假设我们要映射这个类,我们需要创建如下的 SqlMap。通过使用查询,稍后我们可以用email地址来查找对应的用户。Account.xml 如下:

<sql-map name="Account">

  <result-map name="result" class="examples.Account">
    <property name="name" column="NAME" columnIndex="1"/>
    <property name="email" column="EMAIL" columnIndex="2"/>
  </result-map>

  <mapped-statement name="getAccountByEmail" result-map="result">
    select ACCOUNT.NAME, ACCOUNT.EMAIL
    from ACCOUNT
    where ACCOUNT.EMAIL = #value#
  </mapped-statement>

  <mapped-statement name="insertAccount">
    insert into ACCOUNT (NAME, EMAIL) values (#name#, #email#)
  </mapped-statement>

</sql-map>

定义完Sql Map之后,我们需要创建一个iBATIS的配置文件(sqlmap-config.xml):

<sql-map-config>

  <sql-map resource="example/Account.xml"/>

</sql-map-config>

iBATIS会从CLASSPATH加载资源,所以要确保 Account.xml 在CLASSPATH下。

通过Spring,我们可以非常容易的使用 SqlMapFactoryBean 来创建SqlMap:

<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="sqlMap" class="org.springframework.orm.ibatis.SqlMapFactoryBean">
    <property name="configLocation" value="WEB-INF/sqlmap-config.xml"/>
  </bean>

  ...
</beans>

12.6.2.2. 使用 SqlMapTemplateSqlMapDaoSupport

SqlMapDaoSupport 类是一个类似于 HibernateDaoSupportJdoDaoSupport 的支持类。我们来实现一个DAO:

public class SqlMapAccountDao extends SqlMapDaoSupport implements AccountDao {

    public Account getAccount(String email) throws DataAccessException {
        return (Account) getSqlMapTemplate().executeQueryForObject("getAccountByEmail", email);
    }

    public void insertAccount(Account account) throws DataAccessException {
        getSqlMapTemplate().executeUpdate("insertAccount", account);
    }
}

正如你所看到的,我们使用预先配置好的 SqlMapTemplate 来执行查询。 Spring在创建 SqlMapAccountDao 的时候已经使用 SqlMapFactoryBean 为我们初始化了 SqlMap,如下所示一切都准备就绪了。 注意在iBATIS SQL Maps 1.x里面,JDBC DataSource 通常都是DAO中指定的。

<beans>
  ...

  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="dataSource" ref="dataSource"/>
    <property name="sqlMap" ref="sqlMap"/>
  </bean>

</beans>

注意 SqlMapTemplate 实例是可以手工创建的,通过传入 DataSource,并把 SqlMap 作为构造函数参数进行创建。 SqlMapDaoSupport 的基类已经预先替我们初始化了一个 SqlMapTemplate 实例了。

12.6.3. iBATIS SQL Maps 2.x

12.6.3.1. 创建SqlMapClient

如果我们希望使用iBATIS 2.x来映射刚才的那个Account类,则需要创建这样一个SQL map Account.xml

<sqlMap namespace="Account">

  <resultMap id="result" class="examples.Account">
    <result property="name" column="NAME" columnIndex="1"/>
    <result property="email" column="EMAIL" columnIndex="2"/>
  </resultMap>

  <select id="getAccountByEmail" resultMap="result">
    select ACCOUNT.NAME, ACCOUNT.EMAIL
    from ACCOUNT
    where ACCOUNT.EMAIL = #value#
  </select>

  <insert id="insertAccount">
    insert into ACCOUNT (NAME, EMAIL) values (#name#, #email#)
  </insert>

</sqlMap>

iBATIS 2的配置文件有了一些改变(sqlmap-config.xml):

<sqlMapConfig>

  <sqlMap resource="example/Account.xml"/>

</sqlMapConfig>

记住iBATIS从CLASSPATH下加载资源,所以必须确保 Account.xml 在CLASSPATH下。

我们可以使用Spring application context中的 SqlMapClientFactoryBean。 注意iBATIS SQL Map 2.x中,JDBC DataSource 通常由 SqlMapClientFactoryBean 指定,并开启了延迟加载。

<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="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="WEB-INF/sqlmap-config.xml"/>
    <property name="dataSource" ref="dataSource"/>
  </bean>

  ...
</beans>

12.6.3.2. 使用 SqlMapClientTemplateSqlMapClientDaoSupport

SqlMapClientDaoSupport 提供了类似 SqlMapDaoSupport 的功能。我们可以继承它来实现我们自己的DAO:

public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {

    public Account getAccount(String email) throws DataAccessException {
        return (Account) getSqlMapClientTemplate().queryForObject("getAccountByEmail", email);
    }

    public void insertAccount(Account account) throws DataAccessException {
        getSqlMapClientTemplate().update("insertAccount", account);
    }
}

我们可以在application context中创建了 SqlMapAccountDao 并且注入 SqlMapClient 实例,这样我们就可以在DAO中使用预先配置的 SqlMapClientTemplate 来执行查询了:

<beans>
  ...

  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="sqlMapClient" ref="sqlMapClient"/>
  </bean>

</beans>

注意 SqlMapTemplate 实例也可以手工创建,使用 SqlMapClient 作为构造函数参数。 SqlMapClientDaoSupport 基类为我们预先初始化了一个 SqlMapClientTemplate 实例。

SqlMapClientTemplate 还提供了一个通用的 execute 方法,将用户自定义的 SqlMapClientCallback 的实现作为参数。 举例来说,这可以实现批量操作:

public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {
    ...

    public void insertAccount(Account account) throws DataAccessException {
        getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
            public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
                executor.startBatch();
                executor.update("insertAccount", account);
                executor.update("insertAddress", account.getAddress());
                executor.executeBatch();
            }
        });
    }
}

一般来说,任何由 SqlMapExecutor API提供的操作组合都以这样的回调形式被使用。 而在这个过程中产生的任何 SQLException 都将被自动地转化为Spring的通用的 DataAccessException 异常体系。

12.6.3.3. 基于原生的iBATIS API的DAO实现

你也可以基于原生的iBATIS API来编程,而无需对Spring产生任何依赖。直接使用注入的 SqlMapClient。 一个相应的DAO实现类看上去就像下面这样:

public class SqlMapAccountDao implements AccountDao {

    private SqlMapClient sqlMapClient;

    public void setSqlMapClient(SqlMapClient sqlMapClient) {
        this.sqlMapClient = sqlMapClient;
    }

    public Account getAccount(String email) {
        try {
            return (Account) this.sqlMapClient.queryForObject("getAccountByEmail", email);
        }
        catch (SQLException ex) {
            throw new MyDaoException(ex);
        }
    }

    public void insertAccount(Account account) throws DataAccessException {
        try {
            this.sqlMapClient.update("insertAccount", account);
        }
        catch (SQLException ex) {
            throw new MyDaoException(ex);
        }
    }
}

在这种情况下,由iBATIS API抛出的 SQLException 异常需要以用户自定义的方式进行处理:通常封装成为你的应用程序自身的DAO异常。 在application context中进行的整合看上去依然像以前一样,这是由于基于原生的iBATIS的DAO依然遵循IoC的模式:

<beans>
  ...

  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="sqlMapClient" ref="sqlMapClient"/>
  </bean>

</beans>

12.7. JPA

位于 org.springframework.orm.jpa 包下的Spring JPA相关类提供了与Hibernate和JDO类似的针对 Java Persistence API 的简单支持。 Spring提供了一些实现类来支持额外的特性。

12.7.1. 在Spring环境中建立JPA

Spring JPA 提供了两种方法创建JPA EntityManagerFactory

12.7.1.1.  LocalEntityManagerFactoryBean

LocalEntityManagerFactoryBean 负责创建一个适合于当前环境的 EntityManager 来使用JPA进行数据访问。 factory bean将使用JPA PersistenceProvider 类的自动检测机制,而在绝大多数情况下,仅仅需要一个persistence unit名称配置:

<beans>
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
 <property name="entityManagerName" value="myPersistenceUnit"/>
</bean>
...
</beans>

切换到一个JNDI的 EntityManagerFactory (举例来说,在JTA环境中),只需要简单更改XML配置:

<beans>
...
<jndi:lookup id="entityManagerFactory" jndi-name="jpa/myPersistenceUnit"/>
...
</beans>

12.7.1.2.  LocalContainerEntityManagerFactoryBean

LocalContainerEntityManagerFactoryBean 提供了对JPA EntityManagerFactory 的完整控制,并且非常适合那种有简单用户定制需要的环境。 LocalContainerEntityManagerFactoryBean 将基于 'persistence.xml' 文件、提供的 dataSourceLookup 策略和 loadTimeWeaver 创建一个 PersistenceUnitInfo 类。 这就是为何它能够在JNDI之外的用户定义的数据源之上工作,并且能够控制织入流程。

<beans>
 ...
 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSources">
   <map>
    <entry key="dataSource1" value-ref="someDataSource"/>
    <entry key="dataSource2" value-ref="anotherDataSource"/>
   </map>
  </property>
  <property name="loadTimeWeaver">
    <bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver"/>
  </property>
 </bean>
</beans>

LoadTimeWeaver 接口是由Spring提供的允许JPA ClassTransformer 实例能够插入到特定的应用环境中的支持类(web容器/应用服务器)。 通过一个JDK 5.0的 代理 挂钩,在典型情况下不是非常有效率。 代理通常在 整个虚拟机 环境下工作,并且监控 每一个 被加载的类 - 有时这在生产环境下是不提倡的。

Spring提供了许多在不同环境中的 LoadTimeWeaver 实现类,允许 ClassTransformer 实例能够仅仅在 每个classloader 而不是每个虚拟机上被应用。

12.7.1.2.1. 在Tomcat上创建

Jakarta Tomcat 默认的类装载器并不支持类的切换,但是它允许使用用户自定义的类装载器。 Spring提供了 TomcatInstrumentableClassLoader 类(位于 org.springframework.instrument.classloading.tomcat 包中), 这个类继承自Tomcat的类装载器(WebappClassLoader)并且允许JPA ClassTransformer 的实例来“增强”所有由它加载的类。 简单来说,JPA转化器(JPA transformer)仅仅在特定的web应用中才能被使用。(使用 TomcatInstrumentableClassLoader 的那些应用)。

为了使用用户自定义的类装载器:

  1. spring-tomcat-weaver.jar 复制到 $CATALINA_HOME/server/lib 下(其中$CATALINA_HOME 表示Tomcat的安装路径)。

  2. 通过修改web application context使Tomcat使用用户自定义的类装载器(而不是默认的类装载器):

    <Context path="/myWebApp" docBase="/my/webApp/location" ...>
       <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
    ...
    </Context>

    Tomcat 5.0.x 和 5.5.x 系列支持多个上下文路径(context locations):全局的上下文路径($CATALINA_HOME/conf/context.xml)会影响所有被部署的web应用、每个单独部署在Server上的web应用的配置 ($CATALINA_HOME/conf/[enginename]/[hostname]/my-webapp-context.xml)或者跟随着web应用的配置(your-webapp.war/META-INF/context.xml)。 从效率的角度说,我们推荐后者的配置方式,因为仅仅使用JPA的应用会使用用户自定义的类装载器。更多具体有关可用的上下文路径的内容请参见Tomcat 5.x的文档

    在Tomcat 4.x中,你可以使用相同的context.xml并将它放到 $CATALINA_HOME/webapps 下,或者修改 $CATALINA_HOME/conf/server.xml 来使用用户自定义的类装载器。 更多信息请参看Tomcat 4.x的文档

  3. 在配置 LocalContainerEntityManagerFactoryBean 时,使用合适的LoadTimeWeaver

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      ...
      <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
      </property>
      ...
    </bean>

通过使用这种技术,JPA应用依赖于特定的“仪器”,从而无需任何代理可以在Tomcat中运行。 当主应用程序依赖于不同JPA实现时这点显得尤为重要,因为JPA转换器只在classloader级别运行并各自独立

12.7.2.  JpaTemplateJpaDaoSupport

每个基于JPA的DAO将通过IoC接收一个 EntityManagerFactory 实例。 这样的DAO可以通过 EntityManagerFactory 来操作原生JPA的API进行数据访问,也可以直接使用Spring的 JpaTemplate

<beans>
...
  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
...
</beans>
public class JpaProductDao implements ProductDao {

    private EntityManagerFactory entityManagerFactory;

    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.entityManagerFactoryeManagerFactory = emf;
    }

    public Collection loadProductsByCategory(final String category) throws DataAccessException {
        JpaTemplate jpaTemplate = new JpaTemplate(this.entityManagerFactory);

        return (Collection) jpaTemplate.execute(new JpaCallback() {

            public Object doInJpa(EntityManager em) throws PersistenceException {

                Query query = em.createQuery("from Product");
                List result = query.execute(category);
                // do some further stuff with the result list
                return result;
            }
        });
    }
}

JpaCallback 实现允许所有类型的JPA数据访问。 JpaTemplate 将确保 EntityManager 正确的打开和关闭,并且能够自动地参与到事务中去。 除此之外,JpaTemplate 能够恰当地处理异常,确保资源的及时清理以及必要时的事务回滚。 Template实例不仅是线程安全的,而且它是可重用的,因而它能够作为实例变量被一个类持有。 注意 JpaTemplate 提供了简单的诸如find、load、merge等操作的快捷函数来替代默认的回调实现。

不仅如此,Spring还提供了一个方便的 JpaDaoSupport 基类,提供了 get/setEntityManagerFactory 方法以及 getJpaTemplate() 方法供子类调用:

public class ProductDaoImpl extends JpaDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        return getJpaTemplate().find("from Product");
    }
}

除了直接使用Spring的 JpaTemplate,你也可以使用原生JPA的API来实现基于Spring的DAO,此时你需要自行明确地处理EntityManager。 正如在相应的Hibernate章节描述的一样,这种做法的主要优点在于你的数据访问代码可以在整个过程中抛出checked exceptions。JpaDaoSupport 为这种情况提供了多种函数支持,包括获取和释放一个具备事务管理功能的 EntityManager 和相关的异常转化。

12.7.3. 基于原生的JPA实现DAO

你完全可以使用原生的JPA的API进行编程而无需对Spring产生任何依赖,这可以通过一个被注入的 EntityManagerFactoryEntityManager 来完成。 注意Spring能够识别字段或者方法级别的 @PersistenceUnit@PersistenceContext 标注,如果 PersistenceAnnotationBeanPostProcessor 功能被激活的话。 一个相应的DAO实现类看上去就像这样:

public class ProductDaoImpl implements ProductDao {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    public Collection loadProductsByCategory(String category) {
        EntityManager em = this.entityManagerFactory.getEntityManager();
        try {
             Query query = em.createQuery("from Product");
             return result = query.execute(category);
        }
        finally {
            if (em != null) {
                em.close();
            }
        }
    }
}

上述的DAO不对Spring产生任何依赖,而它就如同使用Spring的 JpaTemplate 那样,非常适合在Spring的application context中进行配置。 此外,这样的DAO可以利用标注来要求 EntityManagerFactory 的注入:

<beans>
  ...
  <!-- JPA annotations bean post processor -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

这类DAO的主要问题在于每次总是从工厂中获取一个新的 EntityManager 实例。 这一点可以通过对 EntityManager 而不是factory进行注入来解决:

public class ProductDaoImpl implements ProductDao {

    private EntityManager em;

    @PersistenceContext
    public void setEntityManager(EntityManager em) {
        this.em = em;
    }

    public Collection loadProductsByCategory(String category) {
       Query query = em.createQuery("from Product");
       return result = query.execute(category);
    }
}

被注入的 EntityManager 是受到Spring管理的(能够感知当前的事务)。 非常值得注意的是,即使这种新的实现更倾向于方法级别的注入(使用 EntityManager 而不是 EntityManagerFactory), 对于注解的使用,application context的XML配置无需任何改变。

这种DAO风格的主要优点在于它仅仅依赖JPA,而无需依赖任何的Spring的类。除此之外,JPA的标注是被识别的,注入能够被Spring容器自动应用。 从无入侵性的角度来说,这一点非常有吸引力,它对于JPA开发者来说也更自然。

12.7.4. 异常转化

DAO不仅会抛出普通的 PersistenceException 异常,(这是一个无需声明和捕获的unchecked exception),还会抛出诸如 IllegalArgumentExceptionIllegalStateException 之类的异常。 这意味着,DAO的调用者只能以普通的错误来处理这些异常,除非完全依赖JPA自身的异常体系。因而,除非你将DAO的调用者绑定到具体的实现策略上去,否则你将无法捕获特定的异常原因(诸如乐观锁异常)。 这种折中平衡或许可以被接受,如果你的应用完全基于JPA或者无需进行特殊的异常处理。不过,Spring提供了一个允许你进行透明的异常转化的解决方案:通过使用 @Repository 注解:

@Repository
public class ProductDaoImpl implements ProductDao {
 ...
}
<beans>
  ...
  <!-- Exception translation bean post processor -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

后置处理器将自动的寻找所有的异常转化器(PersistenceExceptionTranslator 这个接口的实现类)并通知所有打上 @Repository 注解的bean,从而能够使得被找到的异常转化器能够在抛出异常时做相应的异常转化工作。

总结来说:DAO能够基于普通的Java持久层API和注解来实现,但同样也能享受到由Spring管理事务、IoC和透明的异常转化(转化成为Spring的异常体系)等好处。

12.7.5. 事务管理

将事务管理纳入到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.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">
  ...

  <bean id="myTxManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="myEmf"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </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>

</beans>

Spring JPA允许将一个JPA事务暴露给访问相同的JDBC DataSource 的JDBC访问代码,前提条件是,被注册的 JpaDialect 能够支持获取底层的JDBC Connection。 Spring提供了针对Toplink和Hibernate的JPA实现的Dialect。具体参见下一节有关 JpaDialect 的机制。

12.7.6. JpaDialect

作为一个高级特性,JpaTemplateJpaTransactionManagerAbstractEntityManagerFactoryBean 的子类支持用户自定义的 JpaDialect 作为"jpaDialect"的bean属性进行注入。 在这种情况下,DAO将不再接收 EntityManagerFactory 的引用作为参数,而是接收一个完整的 JpaTemplate(也就是将它注入到 JpaDaoSupport 的"jpaTemplate"属性中去 一个 JpaDialect 实现能够激活一些由Spring支持的高级特性,这通常由特定的实现供应商指定: 一个 JpaDialect 实现能够激活一些由Spring支持的高级特性,这通常由特定的实现供应商指定:

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

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

  • PersistenceExceptions 到Spring的 DataAccessExceptions 高级转化

这对于特殊的事务语义和异常的高级转化这样的高级特性比较有价值。注意默认的实现(使用DefaultJpaDialect)并不提供任何特殊的功能。如果你需要上述的特殊功能,你必须指定合适的Dialect。

更多有关它的操作以及它如何在Spring的JPA支持中使用的详细信息请参看 JpaDialect 的Javadoc。

Sponsored by