Spring JDBC-混合框架的事務管理
- ?
Spring 抽象的 DAO 體系兼容多種數據訪問技術,它們各有特色,各有千秋。
-
Hibernate 是非常優秀的 ORM 實現方案,但對底層 SQL 的控制不太方便
-
MyBatis 則通過模板化技術讓我們能方便地控制 SQL,但沒有 Hibernate 那樣高的開發效率
-
自由度最高的當然是直接使用 Spring JDBC 莫屬了,但是它也是最底層的,靈活的代價是代碼的繁復
很難說哪種數據訪問技術是最優秀的,只有在某種特定的場景下,才能給出答案。所以在一個應用中,往往采用多個數據訪問技術:一般是兩種,一種采用 ORM 技術框架,而另一種采用偏 JDBC 的底層技術。
問題
當我們采用:ORM 技術框架+ 偏 JDBC 的底層技術如何應對事務管理的問題呢? 我們知道 Spring 為每種數據訪問技術提供了相應的事務管理器,難道需要分別為它們配置對應的事務管理器嗎?它們到底是如何協作,如何工作的呢?
解決方案
Spring 事務管理的為我們的提供了解決方案。
當我們采用了一個高端 ORM 技術(Hibernate,JPA,JDO),同時采用一個 JDBC 技術(Spring JDBC,MyBatis),由於前者的會話(Session)是對後者連接(Connection)的封裝,Spring 會“足夠智能地”在同一個事務線程讓前者的會話封裝後者的連接。所以,我們只要直接采用前者的事務管理器就可以了。
我們列舉下混合數據訪問技術所對應的事務管理器:
示例:Hibernate + Spring JDBC
由於一般不會出現同時使用多個 ORM 框架的情況(如 Hibernate + JPA),我們不擬對此命題展開論述,只重點研究 ORM 框架 + JDBC 框架的情況。
Hibernate + Spring JDBC 可能是被使用得最多的組合,我們通過實例來觀察事物的運行情況。
User 使用了註解聲明的實體類
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 使用 Hibernate 數據訪問技術
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 數據訪問技術
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());
}
}
關鍵配置文件
<!-- 使用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(com.artisan.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>
日誌:
21:37:57,062 (AbstractPlatformTransactionManager.java:365) - Creating new transaction
with name [com.artisan.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 21:37:57,093 (SessionImpl.java:220) - opened session at timestamp: 12666407370 21:37:57,093 (HibernateTransactionManager.java:493) - Opened new Session
[org.hibernate.impl.SessionImpl@83020] for Hibernate transaction ① 21:37:57,093 (HibernateTransactionManager.java:504) - Preparing JDBC Connection
of Hibernate Session [org.hibernate.impl.SessionImpl@83020] 21:37:57,109 (JDBCTransaction.java:54) - begin
…
logon method...
updateLastLogonTime...
… 21:37: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=?
… 21:37:57,187 (HibernateTemplate.java:422) - Not closing pre-bound
Hibernate Session after HibernateTemplate 21:37:57,187 (HibernateTemplate.java:397) - Found thread-bound Session
for HibernateTemplate Hibernate: update T_USER set LAST_LOGON_TIME=?, password=?, score=? where USER_NAME=?
… 2017-09-26 21:37:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:470)
- Participating in existing transaction ②
addScore... 2017-09-26 21:37:57,203 DEBUG [main] (JdbcTemplate.java:785)
- Executing prepared SQL update 2017-09-26 21:37:57,203 DEBUG [main] (JdbcTemplate.java:569)
- Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 2017-09-26 21:37:57,203 DEBUG [main] (JdbcTemplate.java:794)
- SQL update affected 1 rows
激活連接數量:1 ③ 2017-09-26 21:37:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:752)
- Initiating transaction commit 2017-09-26 21:37:57,203 DEBUG [main] (HibernateTransactionManager.java:652)
- Committing Hibernate transaction on Session
[org.hibernate.impl.SessionImpl@83020] ④ 2017-09-26 21:37: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+MyBatis,JPA+Spring JDBC,JDO+Spring JDBC 等
Spring JDBC-混合框架的事務管理