天天看點

Spring事務管理--多個ORM架構在使用時的情況分析

7年前的一個項目了,中間一直有人在維護,也是在這個過程中不斷融入了新的東西,比如就項目的持久化這塊來說,就用了ibatis、mybatis、hibernate、spring JDBC四種混合的架構。究其原因隻能說是曆史遺留問題,就不做過多的解釋了。但是這麼多持久化的架構如何協同工作的,尤其是事務的控制,一個系統中使用如此多的持久化架構是,他們是如何正确的進行事務控制的,甚至有些業務邏輯同時調用了使用這些不同架構的方法。這聽起來确實有些困難。我們知道Spring為每種資料通路技術提供了相應的事務管理器,難道需要分别為它們配置對應的事務管理器嗎?它們到底是如何協作和工作的呢?這些層出不窮的問題往往壓制了開發人員使用聯合軍種的想法。

Spring事務管理器的應對 

Spring抽象的DAO體系相容多種資料通路技術,它們各有特色,各有千秋。像Hibernate是非常優秀的ORM實作方案,但對底層SQL的控制不太友善;而iBatis則通過模闆化技術讓你友善地控制SQL,但沒有Hibernate那樣高的開發效率;自由度最高的當然是直接使用SpringJDBC了,但它也是底層的,靈活的代價是代碼的繁複。很難說哪種資料通路技術是最優秀的,隻有在某種特定的場景下才能給出答案。是以在一個應用中,往往采用多個資料通路技術:一般是兩種,一種采用ORM技術架構,而另一種采用偏JDBC的底層技術,兩者珠聯璧合,形成聯合軍種,共同禦敵。 

Spring事務管理的能力。

如果你采用了一個高端ORM技術(Hibernate、JPA、JDO),同時采用一個JDBC技術(Spring JDBC、iBatis),

由于前者的會話(Session)是對後者連接配接(Connection)的封裝,Spring會“足夠智能地”在同一個事務線程讓前者的會話封裝後者的連接配接。

是以,我們隻要直接采用前者的事務管理器就可以了。

表1給出了混合資料通路技術架構所對應的事務管理器。 

序    号 混合資料通路技術架構 事務管理器
1 Hibernate+ Spring JDBC或iBatis org.springframework.orm.hibernate3.HibernateTransactionManager
2 JPA+Spring JDBC或iBatis org.springframework.orm.jpa.JpaTransactionManager
3 JDO+Spring JDBC或iBatis org.springframework.orm.jdo.JdoTransactionManager

Hibernate+Spring JDBC混合架構的事務管理 

由于一般不會出現同時使用多個ORM架構的情況(如Hibernate+JPA),我們不拟對此命題展開論述,隻重點研究ORM架構+JDBC架構的情況。Hibernate+Spring JDBC可能是被使用得最多的組合,本節我們通過執行個體觀察事務管理的運作情況。 

package com.baobaotao.mixdao;

…

@Service("userService")
public class UserService extends BaseService {
    @Autowired
    private HibernateTemplate hibernateTemplate;

    @Autowired
    private ScoreService scoreService;

    public void logon(String userName) {
       
        //①通過Hibernate技術通路資料
        System.out.println("before updateLastLogonTime()..");
        updateLastLogonTime(userName);
        System.out.println("end updateLastLogonTime()..");
        
        //②通過JDBC技術通路資料
        System.out.println("before scoreService.addScore()..");
        scoreService.addScore(userName, 20);
        System.out.println("end scoreService.addScore()..");
    }
    
    public void updateLastLogonTime(String userName) {
        User user = hibernateTemplate.get(User.class,userName);
        user.setLastLogonTime(System.currentTimeMillis());
        hibernateTemplate.update(user);

        //③這句很重要,請看下文的分析
        hibernateTemplate.flush();
    }
}      

在①處,使用Hibernate操作資料,而在②處調用ScoreService#addScore(),該方法内部使用Spring JDBC操作資料。 

在③處,我們顯式調用了flush()方法,将Session中的緩存同步到資料庫中(即馬上向資料庫發送一條更新記錄的SQL語句)。之是以要顯式執行flush()方法,原因是在預設情況下,Hibernate對資料的更改隻是記錄在一級緩存中,要等到事務送出或顯式調用flush()方法時才會将一級緩存中的資料同步到資料庫中,而送出事務的操作發生在  方法傳回前。如果所有針對資料庫的更改操作都使用Hibernate,這種資料同步的延遲機制并不會産生任何問題。但是,我們在logon()方法中同時采用了Hibernate和Spring JDBC混合資料通路技術,Spring JDBC無法自動感覺Hibernate一級緩存,是以如果不及時調用flush()方法将記錄資料更改的一級緩存同步到資料庫中,則②處通過Spring JDBC進行資料更改的結果将被Hibernate一級緩存中的更改覆寫掉,因為Hibernate一級緩存要等到logon()方法傳回前才同步到資料庫! 

使用SpringJDBC資料通路技術,其代碼如下所示: 

package com.baobaotao.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("scoreService")
public class ScoreService extends BaseService{

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void addScore(String userName, int toAdd) {
        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("[scoreUserService.addScore]激活連接配接數量:"
                         +basicDataSource.getNumActive());
    }
}      

關鍵的配置檔案代碼如下所示: 

…
    <!--①使用Hibernate事務管理器 -->
    <bean id="hiberManager"
          class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <!--②使UserService及ScoreService的公用方法都擁有事務 -->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="serviceJdbcMethod"
                      expression="within(com.baobaotao.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>
    
</beans>      

啟動Spring容器,執行UserService#logon()方法,可以檢視到如下的執行日志: 

before userService.logon().. 

①在執行userService.logon()後,Spring開啟一個事務 
Creating new transaction with name13009379637 
Opened new Session [org.hibernate.impl.SessionImpl@c5f468] for Hibernate transaction 
… 
Exposing Hibernate transaction as JDBC transaction [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost, MySQL-AB JDBC Driver] 
before userService.updateLastLogonTime().. 

②userService.updateLastLogonTime()執行時自動綁定到①處開啟的Session中 
Found thread-bound Session for HibernateTemplate 
loading entity: [com.baobaotao.User#tom] 
about to open PreparedStatement (open PreparedStatements: 0, globally: 0) 
… 
about to close PreparedStatement (open PreparedStatements: 1, globally: 1) 
Not closing pre-bound Hibernate Session after HibernateTemplate 
end updateLastLogonTime().. 

before scoreService.addScore().. 

③scoreService.addScore()執行時綁定到①處開啟的Session中,并加入其所對應的事務中 
Found thread-bound Session [org.hibernate.impl.SessionImpl@c5f468] for Hibernate 
transaction 
Participating in existing transaction 
… 
SQL update affected 1 rows 

④此時資料源隻打開了一個連接配接 
[scoreUserService.addScore]激活連接配接數量:1Initiating transaction commit 
-time cascades 
dirty checking collections 
Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects 
Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections 
listing entities: 
com.baobaotao.User{lastLogonTime=1300937963882, score=10, userName=tom, password=123456} 
re-enabling autocommit 

⑥提效Session底層所綁定的JDBC Connection所對應的事務 
committed JDBC Connection 
transaction completed! 
Closing Hibernate Session [org.hibernate.impl.SessionImpl@c5f468] after transaction 
Closing Hibernate Session 
releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)] 
transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources! 
after userService.logon()..      

仔細觀察這段輸出日志,在①處UserService#logon()開啟一個新的事務。②處的UserService#updateLastLogonTime() 綁定到事務上下文的Session中。③處ScoreService#addScore()方法加入到①處開啟的事務上下文中。④處的輸出是ScoreService#addScore()方法内部的輸出資訊,彙報此時資料源激活的連接配接數為1,這清楚地告訴我們Hibernate和JDBC這兩種資料通路技術在同一事務上下文中“共用”一個連接配接。在⑤處,送出Hibernate事務,接着在⑥處觸發調用底層的Connection送出事務。 

從以上的運作結果,我們可以得出這樣的結論:使用Hibernate事務管理器後,可以混合使用Hibernate和SpringJDBC資料通路技術,它們将工作于同一事務上下文中。但是使用SpringJDBC通路資料時,Hibernate的一級或二級緩存得不到同步,此外,一級緩存延遲資料同步機制可能會覆寫SpringJDBC資料更改的結果。 

由于混合資料通路技術方案存在“事務同步而緩存不同步”的情況,是以最好用Hibernate進行讀寫操作,而隻用SpringJDBC進行讀操作。如用Spring JDBC進行簡要清單的查詢,而用Hibernate對查詢出的資料進行維護。 

如果确實要同時使用Hibernate和Spring JDBC讀寫資料,則必須充分考慮到Hibernate緩存機制引發的問題:必須整體分析資料維護邏輯,根據需要及時調用Hibernate的flush()方法,以免覆寫Spring JDBC的更改,在Spring JDBC更改資料庫時,維護Hibernate的緩存。由于方法調用順序的不同都可能影響資料的同步性,是以很容易發生問題,這會極大提高資料通路程式的複雜性。是以筆者鄭重建議不要同時使用Spring JDBC和Hibernate對資料進行寫操作。 

可以将以上結論推廣到其他混合資料通路技術的方案中,如Hibernate+iBatis、JPA+SpringJDBC、JDO+Spring JDBC等。