Hibernate數(shù)據(jù)庫事務攻略
Hibernate數(shù)據(jù)庫事務有很多值得學習的地方,這里我們主要介紹Hibernate數(shù)據(jù)庫事務聲明,包括介紹 非托管環(huán)境、使用JTA、異常處理等方面。
Hibernate數(shù)據(jù)庫事務聲明
數(shù)據(jù)庫(或者系統(tǒng))事務的聲明總是必須的。在數(shù)據(jù)庫事務之外,就無法和數(shù)據(jù)庫通訊(這可能會讓那些習慣于 自動提交事務模式的開發(fā)人員感到迷惑)。永遠使用清晰的事務聲明,即使只讀操作也是如此。進行顯式的事務聲明并不總是需要的,這取決于你的事務隔離級別和數(shù)據(jù)庫的能力,但不管怎么說,聲明事務總歸有益無害。當然,一個單獨的數(shù)據(jù)庫事務總是比很多瑣碎的事務性能更好,即時對讀數(shù)據(jù)而言也是一樣。
一個Hibernate應用程序可以運行在非托管環(huán)境中(也就是獨立運行的應用程序,簡單Web應用程序, 或者Swing圖形桌面應用程序),也可以運行在托管的J2EE環(huán)境中。在一個非托管環(huán)境中,Hibernate 通常自己負責管理數(shù)據(jù)庫連接池。應用程序開發(fā)人員必須手工設置事務聲明,換句話說,就是手工啟 動,提交,或者回滾數(shù)據(jù)庫事務。一個托管的環(huán)境通常提供了容器管理事務(CMT),例如事務裝配通過可聲 明的方式定義在EJB session beans的部署描述符中。可編程式事務聲明不再需要,即使是 Session 的同步也可以自動完成。
讓持久層具備可移植性是人們的理想,這種移植發(fā)生在非托管的本地資源環(huán)境,與依賴JTA但是使用BMT而非CMT的系統(tǒng)之間。在兩種情況下你都可以使用編程式的事務管理。Hibernate提供了一套稱為Transaction的封裝API, 用來把你的部署環(huán)境中的本地事務管理系統(tǒng)轉換到Hibernate事務上。這個API是可選的,但是我們強烈 推薦你使用,除非你用CMT session bean。
通常情況下,結束 Session 包含了四個不同的階段:
◆同步session(flush,刷出到磁盤)
◆提交事務
◆關閉session
◆處理異常
session的同步(flush,刷出)前面已經討論過了,我們現(xiàn)在進一步考察在托管和非托管環(huán)境下的事務聲明和異常處理。
1.非托管環(huán)境
如果Hibernat持久層運行在一個非托管環(huán)境中,數(shù)據(jù)庫連接通常由Hibernate的簡單(即非DataSource)連接池機制 來處理。session/transaction處理方式如下所示:
- //Non-managed environment idiom
- Session sess = factory.openSession();
- Transaction tx = null;
- try {
- tx = sess.beginTransaction();
- // do some work
- ...
- tx.commit();
- }
- catch (RuntimeException e) {
- if (tx != null) tx.rollback();
- throw e; // or display error message
- }
- finally {
- sess.close();
- }
你不需要顯式flush() Session - 對commit()的調用會自動觸發(fā)session的同步(取決于session的第 10.10 節(jié) “Session刷出(flush)”)。調用 close() 標志session的結束。close()方法重要的暗示是,session釋放了JDBC連接。這段Java代碼在非托管環(huán)境下和JTA環(huán)境下都可以運行。
更加靈活的方案是Hibernate內置的"current session"上下文管理,前文已經講過:
- // Non-managed environment idiom with getCurrentSession()
- try {
- factory.getCurrentSession().beginTransaction();
- // do some work
- ...
- factory.getCurrentSession().getTransaction().commit();
- }
- catch (RuntimeException e) {
- factory.getCurrentSession().getTransaction().rollback();
- throw e; // or display error message
- }
你很可能從未在一個通常的應用程序的業(yè)務代碼中見過這樣的代碼片斷:致命的(系統(tǒng))異常應該總是 在應用程序“頂層”被捕獲。換句話說,執(zhí)行Hibernate調用的代碼(在持久層)和處理 RuntimeException異常的代碼(通常只能清理和退出應用程序)應該在不同 的應用程序邏輯層。Hibernate的當前上下文管理可以極大地簡化這一設計,你所有的一切就是SessionFactory。 異常處理將在本章稍后進行討論。
請注意,你應該選擇 org.hibernate.transaction.JDBCTransactionFactory (這是默認選項),對第二個例子來說,hibernate.current_session_context_class應該是"thread"
2. 使用JTA
如果你的持久層運行在一個應用服務器中(例如,在EJB session beans的后面),Hibernate獲取 的每個數(shù)據(jù)源連接將自動成為全局JTA事務的一部分。 你可以安裝一個獨立的JTA實現(xiàn),使用它而不使用EJB。Hibernate提供了兩種策略進行JTA集成。
如果你使用bean管理事務(BMT),可以通過使用Hibernate的 Transaction API來告訴 應用服務器啟動和結束BMT事務。因此,事務管理代碼和在非托管環(huán)境下是一樣的。
- // BMT idiom
- Session sess = factory.openSession();
- Transaction tx = null;
- try {
- tx = sess.beginTransaction();
- // do some work
- ...
- tx.commit();
- }
- catch (RuntimeException e) {
- if (tx != null) tx.rollback();
- throw e; // or display error message
- }
- finally {
- sess.close();
- }
如果你希望使用與事務綁定的Session,也就是使用getCurrentSession()來簡化上下文管理,你將不得不直接使用JTA UserTransactionAPI。
- // BMT idiom with getCurrentSession()
- try {
- UserTransaction tx = (UserTransaction)new InitialContext()
- .lookup("java:comp/UserTransaction");
- tx.begin();
- // Do some work on Session bound to transaction
- factory.getCurrentSession().load(...);
- factory.getCurrentSession().persist(...);
- tx.commit();
- }
- catch (RuntimeException e) {
- tx.rollback();
- throw e; // or display error message
- }
在CMT方式下,事務聲明是在session bean的部署描述符中,而不需要編程。 因此,代碼被簡化為:
- // CMT idiom
- Session sess = factory.getCurrentSession();
- // do some work
- ...
在CMT/EJB中甚至會自動rollback,因為假若有未捕獲的RuntimeException從session bean方法中拋出,這就會通知容器把全局事務回滾。這就意味著,在BMT或者CMT中,你根本就不需要使用Hibernate Transaction API ,你自動得到了綁定到事務的“當前”Session。
注意,當你配置Hibernate的transaction factory的時候,在直接使用JTA的時候(BMT),你應該選擇org.hibernate.transaction.JTATransactionFactory,在CMT session bean中選擇org.hibernate.transaction.CMTTransactionFactory。記得也要設置hibernate.transaction.manager_lookup_class。還有,確認你的hibernate.current_session_context_class未設置(為了向下兼容),或者設置為"jta"。
getCurrentSession()在JTA環(huán)境中有一個弊端。對after_statement連接釋放方式有一個警告,這是被默認使用的。因為JTA規(guī)范的一個很愚蠢的限制,Hibernate不可能自動清理任何未關閉的ScrollableResults 或者Iterator,它們是由scroll()或iterate()產生的。你must通過在finally塊中,顯式調用ScrollableResults.close()或者Hibernate.close(Iterator)方法來釋放底層數(shù)據(jù)庫游標。(當然,大部分程序完全可以很容易的避免在JTA或CMT代碼中出現(xiàn)scroll()或iterate()。)
3. 異常處理
如果 Session 拋出異常 (包括任何SQLException), 你應該立即回滾數(shù)據(jù)庫事務,調用 Session.close() ,丟棄該 Session實例。Session的某些方法可能會導致session 處于不一致的狀態(tài)。所有由Hibernate拋出的異常都視為不可以恢復的。確保在 finally 代碼塊中調用close()方法,以關閉掉 Session。
HibernateException是一個非檢查期異常(這不同于Hibernate老的版本), 它封裝了Hibernate持久層可能出現(xiàn)的大多數(shù)錯誤。我們的觀點是,不應該強迫應用程序開發(fā)人員 在底層捕獲無法恢復的異常。在大多數(shù)軟件系統(tǒng)中,非檢查期異常和致命異常都是在相應方法調用 的堆棧的頂層被處理的(也就是說,在軟件上面的邏輯層),并且提供一個錯誤信息給應用軟件的用戶 (或者采取其他某些相應的操作)。請注意,Hibernate也有可能拋出其他并不屬于 HibernateException的非檢查期異常。這些異常同樣也是無法恢復的,應該 采取某些相應的操作去處理。
在和數(shù)據(jù)庫進行交互時,Hibernate把捕獲的SQLException封裝為Hibernate的 JDBCException。事實上,Hibernate嘗試把異常轉換為更有實際含義 的JDBCException異常的子類。底層的SQLException可以 通過JDBCException.getCause()來得到。Hibernate通過使用關聯(lián)到 SessionFactory上的SQLExceptionConverter來 把SQLException轉換為一個對應的JDBCException 異常的子類。默認情況下,SQLExceptionConverter可以通過配置dialect 選項指定;此外,也可以使用用戶自定義的實現(xiàn)類(參考javadocs SQLExceptionConverterFactory類來了解詳情)。標準的 JDBCException子類型是:
◆JDBCConnectionException - 指明底層的JDBC通訊出現(xiàn)錯誤
◆SQLGrammarException - 指明發(fā)送的SQL語句的語法或者格式錯誤
◆ConstraintViolationException - 指明某種類型的約束違例錯誤
◆LockAcquisitionException - 指明了在執(zhí)行請求操作時,獲取 所需的鎖級別時出現(xiàn)的錯誤。
◆GenericJDBCException - 不屬于任何其他種類的原生異常
4. 事務超時
EJB這樣的托管環(huán)境有一項極為重要的特性,而它從未在非托管環(huán)境中提供過,那就是事務超時。在出現(xiàn)錯誤的事務行為的時候,超時可以確保不會無限掛起資源、對用戶沒有交代。在托管(JTA)環(huán)境之外,Hibernate無法完全提供這一功能。但是,Hiberante至少可以控制數(shù)據(jù)訪問,確保數(shù)據(jù)庫級別的死鎖,和返回巨大結果集的查詢被限定在一個規(guī)定的時間內。在托管環(huán)境中,Hibernate會把事務超時轉交給JTA。這一功能通過Hibernate Transaction對象進行抽象。
- Session sess = factory.openSession();
- try {
- //set transaction timeout to 3 seconds
- sess.getTransaction().setTimeout(3);
- sess.getTransaction().begin();
- // do some work
- ...
- sess.getTransaction().commit()
- }
- catch (RuntimeException e) {
- sess.getTransaction().rollback();
- throw e; // or display error message
- }
- finally {
- sess.close();
- }
注意setTimeout()不應該在CMT bean中調用,此時事務超時值應該是被聲明式定義的。
【編輯推薦】