程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Hibernate的事務和並發(二)

Hibernate的事務和並發(二)

編輯:關於JAVA

12.2.1.非托管環境

如果Hibernat持久層運行在一個非托管環境中,數據庫連接通常由Hibernate的連接池機制 來處理。

代碼內容
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()的調用會自動觸發session的同步。

調用 close() 標志session的結束。 close()方法重要的暗示是,session釋放了JDBC連接。

這段Java代碼是可移植的,可以在非托管環境和JTA環境中運行。

你很可能從未在一個標准的應用程序的業務代碼中見過這樣的用法;致命的(系統)異常應該總是 在應用程序“頂層”被捕獲。換句話說,執行Hibernate調用的代碼(在持久層)和處理 RuntimeException異常的代碼(通常只能清理和退出應用程序)應該在不同 的應用程序邏輯層。這對於你設計自己的軟件系統來說是一個挑戰,只要有可能,你就應該使用 J2EE/EJB容器服務。異常處理將在本章稍後進行討論。

請注意,你應該選擇 org.hibernate.transaction.JDBCTransactionFactory (這是默認選項).

12.2.2.使用JTA

如果你的持久層運行在一個應用服務器中(例如,在EJB session beans的後面),Hibernate獲取 的每個數據源連接將自動成為全局JTA事務的一部分。Hibernate提供了兩種策略進行JTA集成。

如果你使用bean管理事務(BMT),可以通過使用Hibernate的 Transaction API來告訴 應用服務器啟動和結束BMT事務。因此,事務管理代碼和在非托管環境下是一樣的。

代碼內容
// 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();
}
在CMT方式下,事務聲明是在session bean的部署描述符中,而不需要編程。 除非你設置了屬性hibernate.transaction.flush_before_completion和 hibernate.transaction.auto_close_session為true, 否則你必須自己同步和關閉Session。Hibernate可以為你自動同步和關閉 Session。你唯一要做的就是當發生異常時進行事務回滾。幸運的是, 在一個CMT bean中,事務回滾甚至可以由容器自動進行,因為由session bean方法拋出的未處理的 RuntimeException異常可以通知容器設置全局事務回滾。這意味著 在CMT中,你完全無需使用Hibernate的Transaction API 。

請注意,當你配置Hibernate事務工廠的時候,在一個BMT session bean中,你應該選擇 org.hibernate.transaction.JTATransactionFactory,在一個 CMT session bean中選擇org.hibernate.transaction.CMTTransactionFactory。 記住,同時也要設置org.hibernate.transaction.manager_lookup_class。

如果你使用CMT環境,並且讓容器自動同步和關閉session,你可能也希望在你代碼的不同部分使用 同一個session。一般來說,在一個非托管環境中,你可以使用一個ThreadLocal 變量來持有這個session,但是單個EJB方法調用可能會在不同的線程中執行(舉例來說,一個session bean調用另一個session bean)。如果你不想在應用代碼中被傳遞Session對 象實例的問題困擾的話,那麼SessionFactory 提供的 getCurrentSession()方法就很適合你,該方法返回一個綁定到JTA事務 上下文環境中的session實例。這也是把Hibernate集成到一個應用程序中的最簡單的方法!這個“當 前的”session總是可以自動同步和自動關閉(不考慮上述的屬性設置)。我們的session/transaction 管理代碼減少到如下所示:

代碼內容
// CMT idiom
Session sess = factory.getCurrentSession();
// do some work
...

換句話來說,在一個托管環境下,你要做的所有的事情就是調用 SessionFactory.getCurrentSession(),然後進行你的數據訪問,把其余的工作 交給容器來做。事務在你的session bean的部署描述符中以可聲明的方式來設置。session的生命周期完全 由Hibernate來管理。

對after_statement連接釋放方式有一個警告。因為JTA規范的一個很愚蠢的限制,Hibernate不可能自動清理任何未關閉的ScrollableResults 或者Iterator,它們是由scroll()或iterate()產生的。你must通過在finally塊中,顯式調用ScrollableResults.close()或者Hibernate.close(Iterator)方法來釋放底層數據庫游標。(當然,大部分程序完全可以很容易的避免在CMT代碼中出現scroll()或iterate()。)

12.2.3.異常處理

如果 Session 拋出異常 (包括任何SQLException), 你應該立即回滾數據庫事務,調用 Session.close() ,丟棄該 Session實例。Session的某些方法可能會導致session 處於不一致的狀態。所有由Hibernate拋出的異常都視為不可以恢復的。確保在 finally 代碼塊中調用close()方法,以關閉掉 Session。

HibernateException是一個非檢查期異常(這不同於Hibernate老的版本), 它封裝了Hibernate持久層可能出現的大多數錯誤。我們的觀點是,不應該強迫應用程序開發人員 在底層捕獲無法恢復的異常。在大多數軟件系統中,非檢查期異常和致命異常都是在相應方法調用 的堆棧的頂層被處理的(也就是說,在軟件上面的邏輯層),並且提供一個錯誤信息給應用軟件的用戶 (或者采取其他某些相應的操作)。請注意,Hibernate也有可能拋出其他並不屬於 HibernateException的非檢查期異常。這些異常同樣也是無法恢復的,應該 采取某些相應的操作去處理。

在和數據庫進行交互時,Hibernate把捕獲的SQLException封裝為Hibernate的 JDBCException。事實上,Hibernate嘗試把異常轉換為更有實際含義 的JDBCException異常的子類。底層的SQLException可以 通過JDBCException.getCause()來得到。Hibernate通過使用關聯到 SessionFactory上的SQLExceptionConverter來 把SQLException轉換為一個對應的JDBCException 異常的子類。默認情況下,SQLExceptionConverter可以通過配置dialect 選項指定;此外,也可以使用用戶自定義的實現類(參考javadocs SQLExceptionConverterFactory類來了解詳情)。標准的 JDBCException子類型是:

JDBCConnectionException - 指明底層的JDBC通訊出現錯誤

SQLGrammarException - 指明發送的SQL語句的語法或者格式錯誤

ConstraintViolationException - 指明某種類型的約束違例錯誤

LockAcquisitionException - 指明了在執行請求操作時,獲取 所需的鎖級別時出現的錯誤。

GenericJDBCException - 不屬於任何其他種類的原生異常

12.3.樂觀並發控制(Optimistic concurrency control)

唯一能夠同時保持高並發和高可伸縮性的方法就是使用帶版本化的樂觀並發控制。版本檢查使用版本號、 或者時間戳來檢測更新沖突(並且防止更新丟失)。Hibernate為使用樂觀並發控制的代碼提供了三種可 能的方法,應用程序在編寫這些代碼時,可以采用它們。我們已經在前面應用程序長事務那部分展示了 樂觀並發控制的應用場景,此外,在單個數據庫事務范圍內,版本檢查也提供了防止更新丟失的好處。

12.3.1.應用程序級別的版本檢查(Application version checking)

未能充分利用Hibernate功能的實現代碼中,每次和數據庫交互都需要一個新的 Session,而且開發人員必須在顯示數據之前從數據庫中重 新載入所有的持久化對象實例。這種方式迫使應用程序自己實現版本檢查來確保 應用程序事務的隔離,從數據訪問的角度來說是最低效的。這種使用方式和 entity EJB最相似。

// foo is an instance loaded by a previous Session

session = factory.openSession();

Transaction t = session.beginTransaction();

int oldVersion = foo.getVersion();

session.load( foo, foo.getKey() ); // load the current state

if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();

foo.setProperty("bar");

t.commit();

session.close();

version 屬性使用 來映射,如果對象 是髒數據,在同步的時候,Hibernate會自動增加版本號。

當然,如果你的應用是在一個低數據並發環境下,並不需要版本檢查的話,你照樣可以使用 這種方式,只不過跳過版本檢查就是了。在這種情況下,最晚提交生效 (last commit wins)就是你的應用程序長事務的默認處理策略。 請記住這種策略可能會讓應用軟件的用戶感到困惑,因為他們有可能會碰上更新丟失掉卻沒 有出錯信息,或者需要合並更改沖突的情況。

很明顯,手工進行版本檢查只適合於某些軟件規模非常小的應用場景,對於大多數軟件應用場景 來說並不現實。通常情況下,不僅是單個對象實例需要進行版本檢查,整個被修改過的關 聯對象圖也都需要進行版本檢查。作為標准設計范例,Hibernate使用長生命周期 Session的方式,或者脫管對象實例的方式來提供自動版本檢查。

12.3.2.長生命周期session和自動版本化

單個 Session實例和它所關聯的所有持久化對象實例都被用於整個 應用程序事務。Hibernate在同步的時候進行對象實例的版本檢查,如果檢測到並發修 改則拋出異常。由開發人員來決定是否需要捕獲和處理這個異常(通常的抉擇是給用戶 提供一個合並更改,或者在無髒數據情況下重新進行業務操作的機會)。

在等待用戶交互的時候, Session 斷開底層的JDBC連接。這種方式 以數據庫訪問的角度來說是最高效的方式。應用程序不需要關心版本檢查或脫管對象實例 的重新關聯,在每個數據庫事務中,應用程序也不需要載入讀取對象實例。

代碼內容
// foo is an instance loaded earlier by the Session
session.reconnect(); // Obtain a new JDBC connection
Transaction t = session.beginTransaction();
foo.setProperty("bar");
t.commit(); // End database transaction, flushing the change and checking the version
session.disconnect(); // Return JDBC connection

foo 對象始終和載入它的Session相關聯。 Session.reconnect()獲取一個新的數據庫連接(或者 你可以提供一個),並且繼續當前的session。Session.disconnect() 方法把session與JDBC連接斷開,把數據庫連接返回到連接池(除非是你自己提供的數據 庫連接)。在Session重新連接上數據庫連接之後,你可以對任何可能被其他事務更新過 的對象調用Session.lock(),設置LockMode.READ 鎖定模式,這樣你就可以對那些你不准備更新的數據進行強制版本檢查。此外,你並不需要 鎖定那些你准備更新的數據。

假若對disconnect()和reconnect()的顯式調用發生得太頻繁了,你可以使用hibernate.connection.release_mode來代替。

如果在用戶思考的過程中,Session因為太大了而不能保存,那麼這種模式是有 問題的。舉例來說,一個HttpSession應該盡可能的小。由於 Session是一級緩存,並且保持了所有被載入過的對象,因此 我們只應該在那些少量的request/response情況下使用這種策略。而且在這種情況下, Session 裡面很快就會有髒數據出現,因此請牢牢記住這一建議。

此外,也請注意,你應該讓與數據庫連接斷開的Session對持久層保持 關閉狀態。換句話說,使用有狀態的EJB session bean來持有Session, 而不要把它傳遞到web層(甚至把它序列化到一個單獨的層),保存在HttpSession中。

12.3.3.脫管對象(deatched object)和自動版本化

這種方式下,與持久化存儲的每次交互都發生在一個新的Session中。 然而,同一持久化對象實例可以在多次與數據庫的交互中重用。應用程序操縱脫管對象實例 的狀態,這個脫管對象實例最初是在另一個Session 中載入的,然後 調用 Session.update(),Session.saveOrUpdate(), 或者 Session.merge() 來重新關聯該對象實例。

代碼內容
// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();

Hibernate會再一次在同步的時候檢查對象實例的版本,如果發生更新沖突,就拋出異常。

如果你確信對象沒有被修改過,你也可以調用lock() 來設置 LockMode.READ(繞過所有的緩存,執行版本檢查),從而取 代 update()操作。

12.3.4.定制自動版本化行為

對於特定的屬性和集合,通過為它們設置映射屬性optimistic-lock的值 為false,來禁止Hibernate的版本自動增加。這樣的話,如果該屬性 髒數據,Hibernate將不再增加版本號。

遺留系統的數據庫Schema通常是靜態的,不可修改的。或者,其他應用程序也可能訪問同一數據 庫,根本無法得知如何處理版本號,甚至時間戳。在以上的所有場景中,實現版本化不能依靠 數據庫表的某個特定列。在的映射中設置 optimistic-lock="all"可以在沒有版本或者時間戳屬性映射的情況下實現 版本檢查,此時Hibernate將比較一行記錄的每個字段的狀態。請注意,只有當Hibernate能夠比 較新舊狀態的情況下,這種方式才能生效,也就是說, 你必須使用單個長生命周期Session模式,而不能使用 session-per-request-with-detached-objects模式。

有些情況下,只要更改不發生交錯,並發修改也是允許的。當你在 的映射中設置optimistic-lock="dirty",Hibernate在同步的時候將只比較有髒 數據的字段。

在以上所有場景中,不管是專門設置一個版本/時間戳列,還是進行全部字段/髒數據字段比較, Hibernate都會針對每個實體對象發送一條UPDATE(帶有相應的 WHERE語句 )的SQL語句來執行版本檢查和數據更新。如果你對關聯實體 設置級聯關系使用傳播性持久化(transitive persistence),那麼Hibernate可能會執行不必 要的update語句。這通常不是個問題,但是數據庫裡面對on update點火 的觸發器可能在脫管對象沒有任何更改的情況下被觸發。因此,你可以在 的映射中,通過設置select-before-update="true" 來定制這一行為,強制Hibernate SELECT這個對象實例,從而保證, 在更新記錄之前,對象的確是被修改過。

12.4.悲觀鎖定(Pessimistic Locking)

用戶其實並不需要花很多精力去擔心鎖定策略的問題。通常情況下,只要為JDBC連接指定一下隔 離級別,然後讓數據庫去搞定一切就夠了。然而,高級用戶有時候希望進行一個排它的悲觀鎖定, 或者在一個新的事務啟動的時候,重新進行鎖定。

Hibernate總是使用數據庫的鎖定機制,從不在內存中鎖定對象!

類LockMode 定義了Hibernate所需的不同的鎖定級別。一個鎖定 可以通過以下的機制來設置:

當Hibernate更新或者插入一行記錄的時候,鎖定級別自動設置為LockMode.WRITE。

當用戶顯式的使用數據庫支持的SQL格式SELECT ... FOR UPDATE 發送SQL的時候,鎖定級別設置為LockMode.UPGRADE

當用戶顯式的使用Oracle數據庫的SQL語句SELECT ... FOR UPDATE NOWAIT 的時候,鎖定級別設置LockMode.UPGRADE_NOWAIT

當Hibernate在“可重復讀”或者是“序列化”數據庫隔離級別下讀取數據的時候,鎖定模式 自動設置為LockMode.READ。這種模式也可以通過用戶顯式指定進行設置。

LockMode.NONE 代表無需鎖定。在Transaction結束時, 所有的對象都切換到該模式上來。與session相關聯的對象通過調用update() 或者saveOrUpdate()脫離該模式。

"顯式的用戶指定"可以通過以下幾種方式之一來表示:

調用 Session.load()的時候指定鎖定模式(LockMode)。

調用Session.lock()。

調用Query.setLockMode()。

如果在UPGRADE或者UPGRADE_NOWAIT鎖定模式下調 用Session.load(),並且要讀取的對象尚未被session載入過,那麼對象 通過SELECT ... FOR UPDATE這樣的SQL語句被載入。如果為一個對象調用 load()方法時,該對象已經在另一個較少限制的鎖定模式下被載入了,那 麼Hibernate就對該對象調用lock() 方法。

如果指定的鎖定模式是READ, UPGRADE 或 UPGRADE_NOWAIT,那麼Session.lock()就 執行版本號檢查。(在UPGRADE 或者UPGRADE_NOWAIT 鎖定模式下,執行SELECT ... FOR UPDATE這樣的SQL語句。)

如果數據庫不支持用戶設置的鎖定模式,Hibernate將使用適當的替代模式(而不是扔出異常)。 這一點可以確保應用程序的可移植性。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved