Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色,各有千秋。像 Hibernate 是非常优秀的 ORM 实现方案,但对底层 SQL 的控制不太方便;而 iBatis 则通过模板化技术让您方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 莫属了,但是它也是最底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下,才能给出答案。所以在一个应用中,往往采用多个数据访问技术:一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术,两者珠联璧合,形成联合军种,共同御敌。
但是,这种联合军种如何应对事务管理的问题呢?我们知道 Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协作,如何工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。
其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果您采用了一个高端 ORM 技术(Hibernate,JPA,JDO),同时采用一个 JDBC 技术(Spring JDBC,iBatis),由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程让前者的会话封装后者的连接。所以,我们只要直接采用前者的事务管理器就可以了。下表给出了混合数据访问技术所对应的事务管理器:

表 1. 混合数据访问技术的事务管理器

混合数据访问技术

事务管理器

ORM 技术框架

JDBC 技术框架

Hibernate

Spring JDBC 或 iBatis

HibernateTransactionManager

JPA

Spring JDBC 或 iBatis

JpaTransactionManager

JDO

Spring JDBC 或 iBatis

JdoTransactionManager



由于一般不会出现同时使用多个 ORM 框架的情况(如 Hibernate + JPA),我们不拟对此命题展开论述,只重点研究 ORM 框架 + JDBC 框架的情况。Hibernate + Spring JDBC 可能是被使用得最多的组合,下面我们通过实例观察事务管理的运作情况。




清单 1.User.java:使用了注解声明的实体类


import javax.persistence.Entity;  

import javax.persistence.Table;  

import javax.persistence.Column;  

import javax.persistence.Id;  

import java.io.Serializable;  


@Entity  

@Table(name="T_USER")  

public class User implements Serializable{  

    @Id 

    @Column(name = "USER_NAME")  

    private String userName;  

    private String password;  

    private int score;  

     

        @Column(name = "LAST_LOGON_TIME") 

    private long lastLogonTime = 0;   

} 




再来看下 UserService 的关键代码: 



清单 2.UserService.java:使用 Hibernate 数据访问技术 

                                 

package user.mixdao; 

import org.springframework.beans.factory.annotation.Autowired; 

import org.springframework.jdbc.core.JdbcTemplate; 

import org.springframework.context.ApplicationContext; 

import org.springframework.context.support.ClassPathXmlApplicationContext; 

import org.springframework.stereotype.Service; 

import org.springframework.orm.hibernate3.HibernateTemplate; 

import org.apache.commons.dbcp.BasicDataSource; 

import user.User; 


@Service("userService") 

public class UserService extends BaseService { 

    @Autowired 

    private HibernateTemplate hibernateTemplate; 


    @Autowired 

    private ScoreService scoreService; 


    public void logon(String userName) { 

        System.out.println("logon method..."); 

        updateLastLogonTime(userName); //①使用Hibernate数据访问技术 

        scoreService.addScore(userName, 20); //②使用Spring JDBC数据访问技术 

    } 


    public void updateLastLogonTime(String userName) { 

        System.out.println("updateLastLogonTime..."); 

        User user = hibernateTemplate.get(User.class,userName); 

        user.setLastLogonTime(System.currentTimeMillis()); 

         
hibernateTemplate.flush();  
//③请看下文的分析 

    } 

} 




在①处,使用 Hibernate 操作数据,而在②处调用 ScoreService#addScore(),该方法内部使用 Spring JDBC 操作数据。 


在③处,我们显式调用了 flush() 方法,将 Session 中的缓存同步到数据库中,这个操作将即时向数据库发送一条更新记录的 SQL 语句。之所以要在此显式执行 flush() 方法,原因是:默认情况下,Hibernate 要在事务提交时才将数据的更改同步到数据库中,而事务提交发生在 logon() 方法返回前。如果所有针对数据库的更改都使用 Hibernate,这种数据同步延迟的机制不会产生任何问题。但是,我们在 logon() 方法中同时采用了 Hibernate 和 Spring JDBC 混合数据访问技术。Spring JDBC 无法自动感知 Hibernate 一级缓存,所以如果不及时调用 flush() 方法将数据更改同步到数据库,则②处通过 Spring JDBC 进行数据更改的结果将被 Hibernate 一级缓存中的更改覆盖掉,因为,一级缓存在 logon() 方法返回前才同步到数据库! 


ScoreService 使用 Spring JDBC 数据访问技术,其代码如下: 



清单 3.ScoreService.java:使用 Spring JDBC 数据访问技术 

                                 

package user.mixdao; 

import org.springframework.beans.factory.annotation.Autowired; 

import org.springframework.jdbc.core.JdbcTemplate; 

import org.springframework.stereotype.Service; 

import org.apache.commons.dbcp.BasicDataSource; 


@Service("scoreUserService") 

public class ScoreService extends BaseService{ 

    @Autowired 

    private JdbcTemplate jdbcTemplate; 

    public void addScore(String userName, int toAdd) { 

        System.out.println("addScore..."); 

        String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?"; 

        jdbcTemplate.update(sql, toAdd, userName); 

        //① 查看此处数据库激活的连接数 

        BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource(); 

        System.out.println("激活连接数量:"+basicDataSource.getNumActive()); 

    } 

} 




Spring 关键的配置文件代码如下所示: 



清单 4. applicationContext.xml 事务配置代码部分 

                                 

<!-- 使用Hibernate事务管理器 --> 

<bean id="hiberManager" 

    class="org.springframework.orm.hibernate3.HibernateTransactionManager" 

    p:sessionFactory-ref="sessionFactory"/> 

     

<!-- 对所有继承BaseService类的公用方法实施事务增强 --> 

<aop:config proxy-target-class="true"> 

    <aop:pointcut id="serviceJdbcMethod" 

        expression="within(user.mixdao.BaseService+)"/> 

    <aop:advisor pointcut-ref="serviceJdbcMethod" 

        advice-ref="hiberAdvice"/> 

</aop:config> 

     

<tx:advice id="hiberAdvice" transaction-manager="hiberManager"> 

    <tx:attributes> 

        <tx:method name="*"/> 

    </tx:attributes> 

</tx:advice> 




启动 Spring 容器,执行 UserService#logon() 方法,可以查看到如下的执行日志: 



清单 5. 代码运行日志 

                                 

12:38:57,062  (AbstractPlatformTransactionManager.java:365) - Creating new transaction  

    with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 


12:38:57,093  (SessionImpl.java:220) - opened session at timestamp: 12666407370 


12:38:57,093  (HibernateTransactionManager.java:493) - Opened new Session  

    [org.hibernate.impl.SessionImpl@83020] for Hibernate transaction ① 


12:38:57,093  (HibernateTransactionManager.java:504) - Preparing JDBC Connection  

    of Hibernate Session [org.hibernate.impl.SessionImpl@83020] 


12:38:57,109  (JDBCTransaction.java:54) - begin 


… 


logon method... 

updateLastLogonTime... 

… 


12:38:57,109  (AbstractBatcher.java:401) - select user0_.USER_NAME as USER1_0_0_,  

    user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_, 

        user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=? 

     

Hibernate: select user0_.USER_NAME as USER1_0_0_,  

        user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_,  

        user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=? 


… 


12:38:57,187  (HibernateTemplate.java:422) - Not closing pre-bound  

    Hibernate Session after HibernateTemplate 


12:38:57,187  (HibernateTemplate.java:397) - Found thread-bound Session 

    for HibernateTemplate 


Hibernate: update T_USER set LAST_LOGON_TIME=?, password=?, score=? where USER_NAME=? 


… 


2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:470)  

    - Participating in existing transaction ② 

addScore... 


2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:785)  

    - Executing prepared SQL update 


2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:569) 

    - Executing prepared SQL statement  

        [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 


2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:794)  

    - SQL update affected 1 rows 


激活连接数量:1 ③ 

2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:752)  

    - Initiating transaction commit 

2010-02-20 12:38:57,203 DEBUG [main] (HibernateTransactionManager.java:652)  

    - Committing Hibernate transaction on Session  

        [org.hibernate.impl.SessionImpl@83020] ④ 


2010-02-20 12:38:57,203 DEBUG [main] (JDBCTransaction.java:103) - commit ⑤


仔细观察这段输出日志,在①处 UserService#logon() 开启一个新的事务,在②处 ScoreService#addScore() 方法加入到①处开启的事务上下文中。③处的输出是 ScoreService#addScore() 方法内部的输出,汇报此时数据源激活的连接数为 1,

这清楚地告诉我们 Hibernate 和 JDBC 这两种数据访问技术在同一事务上下文中“共用”一个连接 。在④处,提交 Hibernate 事务,接着在⑤处触发调用底层的 Connection 提交事务。


从以上的运行结果,我们可以得出这样的结论:使用 Hibernate 事务管理器后,可以混合使用 Hibernate 和 Spring JDBC 数据访问技术,它们将工作于同一事务上下文中。但是使用 Spring JDBC 访问数据时,Hibernate 的一级或二级缓存得不到同步,此外,一级缓存延迟数据同步机制可能会覆盖 Spring JDBC 数据更改的结果。


由于混合数据访问技术的方案的事务同步而缓存不同步的情况,所以最好用 Hibernate 完成读写操作,而用 Spring JDBC 完成读的操作

。 如用 Spring JDBC 进行简要列表的查询,而用 Hibernate 对查询出的数据进行维护。如果确实要同时使用 Hibernate 和 Spring JDBC 读写数据,则必须充分考虑到 Hibernate 缓存机制引发的问题:必须充分分析数据维护逻辑,根据需要,及时调用 Hibernate 的 flush() 方法,以免覆盖 Spring JDBC 的更改,在 Spring JDBC 更改数据库时,维护 Hibernate 的缓存。


可以将以上结论推广到其它混合数据访问技术的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。