Hibernate的事務(wù)和并發(fā)控制很容易掌握。Hibernate直接使用JDBC連接和JTA資源,不添加任何附加鎖定行為。我們強(qiáng)烈推薦你花點(diǎn)時(shí)間了解JDBC編程,ANSI SQL查詢語(yǔ)言和你使用的數(shù)據(jù)庫(kù)系統(tǒng)的事務(wù)隔離規(guī)范。Hibernate只添加自動(dòng)版本管理,而不會(huì)鎖 定內(nèi)存中的對(duì)象,也不會(huì)改變數(shù)據(jù)庫(kù)事務(wù)的隔離級(jí)別。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)來(lái)訪問(wèn)你的數(shù)據(jù)庫(kù)資源。
?除了自動(dòng)版本管理,針對(duì)行級(jí)悲觀鎖定,Hibernate也提供了輔助的API,它使用了 SELECT FOR UPDATE的SQL語(yǔ)法。本章后面會(huì)討論這個(gè)API。
我們從Configuration層、SessionFactory層, 和 Session層開(kāi)始討論Hibernate的并行控制、
數(shù)據(jù)庫(kù)事務(wù)和應(yīng)用 程序的長(zhǎng)事務(wù)。
12.1.Session和事務(wù)范圍(transaction scopes)一個(gè)SessionFactory對(duì)象的創(chuàng)建代價(jià)很昂貴,它是線程
安全的對(duì)象,它被設(shè)計(jì)成可以 為所有的應(yīng)用程序線程所共享。它只創(chuàng)建一次,通常是在應(yīng)用程序啟動(dòng)的時(shí)候,由一個(gè) Configuraion的實(shí)例來(lái)創(chuàng)建。
一個(gè)Session的對(duì)象是輕型的,非線程
安全的,對(duì)于單個(gè)業(yè)務(wù)進(jìn)程,單個(gè)的 工作單元而言,它只被使用一次,然后就丟棄。只有在需要的時(shí)候,Session 才會(huì)獲取一個(gè)JDBC的Connection(或一個(gè)Datasource)對(duì)象。所以你可以放心的打開(kāi)和關(guān)閉Session,甚至當(dāng)你并不確定一個(gè)特定的請(qǐng)求是否需要數(shù)據(jù)訪問(wèn)時(shí),你也可以這樣做。(一旦你實(shí)現(xiàn)下面提到的使用了請(qǐng)求攔截的模式,這就 變得很重要了。
此外我們還要考慮
數(shù)據(jù)庫(kù)事務(wù)。
數(shù)據(jù)庫(kù)事務(wù)應(yīng)該盡可能的短,降低
數(shù)據(jù)庫(kù)鎖定造成的資源爭(zhēng)用。
數(shù)據(jù)庫(kù)長(zhǎng)事務(wù)會(huì)導(dǎo)致你的應(yīng)用程序無(wú)法擴(kuò)展到高的并發(fā)負(fù)載。
一個(gè)操作單元(Unit of work)的范圍是多大?單個(gè)的Hibernate Session能跨越多個(gè)
數(shù)據(jù)庫(kù)事務(wù)嗎?還是一個(gè)Session的作用范圍對(duì)應(yīng)一個(gè)
數(shù)據(jù)庫(kù)事務(wù)的范圍?應(yīng)該何時(shí)打開(kāi) Session,何時(shí)關(guān)閉Session?,你又如何劃分
數(shù)據(jù)庫(kù)事務(wù)的邊界呢?
12.1.1.操作單元(Unit of work)首先,別再用session-per-operation這種反模式了,也就是說(shuō),在單個(gè)線程中, 不要因?yàn)橐淮魏?jiǎn)單的
數(shù)據(jù)庫(kù)調(diào)用,就打開(kāi)和關(guān)閉一次Session!
數(shù)據(jù)庫(kù)事務(wù)也是如此。 應(yīng)用程序中的
數(shù)據(jù)庫(kù)調(diào)用是按照計(jì)劃好的次序,分組為原子的操作單元。(注意,這也意味著,應(yīng)用程序中,在單個(gè)的SQL語(yǔ)句發(fā)送之后,自動(dòng)事務(wù)提交(auto-commit)模式失效了。這種模式專門(mén)為SQL控制臺(tái)操作設(shè)計(jì)的。 Hibernate禁止立即自動(dòng)事務(wù)提交模式,或者期望應(yīng)用服務(wù)器禁止立即自動(dòng)事務(wù)提交模式。)
在多用戶的client/server應(yīng)用程序中,最常用的模式是 每個(gè)請(qǐng)求一個(gè)會(huì)話(session-per-request)。在這種模式下,來(lái)自客戶端的請(qǐng)求被發(fā)送到服務(wù)器端(即Hibernate持久化層運(yùn)行的地方),一 個(gè)新的Hibernate Session被打開(kāi),并且執(zhí)行這個(gè)操作單元中所有的
數(shù)據(jù)庫(kù)操作。 一旦操作完成(同時(shí)發(fā)送到客戶端的響應(yīng)也準(zhǔn)備就緒),session被同步,然后關(guān)閉。你也可以使用單 個(gè)
數(shù)據(jù)庫(kù)事務(wù)來(lái)處理客戶端請(qǐng)求,在你打開(kāi)Session之后啟動(dòng)事務(wù),在你關(guān)閉 Session之前提交事務(wù)。會(huì)話和請(qǐng)求之間的關(guān)系是一對(duì)一的關(guān)系,這種模式對(duì) 于大多數(shù)應(yīng)用程序來(lái)說(shuō)是很棒的。
真正的挑戰(zhàn)在于如何去實(shí)現(xiàn)這種模式:不僅Session和事務(wù)必須被正確的開(kāi)始和結(jié)束,而且他們也必須能被數(shù)據(jù)訪問(wèn)操作訪問(wèn)。用攔截器來(lái)實(shí)現(xiàn)操作單元的劃分,該攔截器在客戶端請(qǐng)求達(dá)到服務(wù)器端的時(shí)候開(kāi)始,在服務(wù)器端發(fā)送響應(yīng)(即,ServletFilter)之前結(jié)束。我們推薦 使用一個(gè)ThreadLocal 變量,把 Session綁定到處理客戶端請(qǐng)求的線 程上去。這種方式可以讓運(yùn)行在該線程上的所有程序代碼輕松的訪問(wèn)Session(就像訪問(wèn)一個(gè)靜態(tài)變量那樣)。你也可以在一個(gè)ThreadLocal 變量中保持事務(wù)上下文環(huán)境,不過(guò)這依賴 于你所選擇的
數(shù)據(jù)庫(kù)事務(wù)劃分機(jī)制。這種實(shí)現(xiàn)模式被稱之為 ThreadLocal Session和 Open Session in View。你可以很容易的擴(kuò)展本文前面章節(jié)展示的 HibernateUtil 輔助類來(lái)實(shí)現(xiàn)這種模式。當(dāng)然,你必胝業(yè)揭恢質(zhì)迪擲菇仄韉姆椒ǎ?且可以把攔截器集成到你的應(yīng)用環(huán)境中。請(qǐng)參考Hibernate
網(wǎng)站上面的提示和例子。
12.1.2.應(yīng)用程序事務(wù)(Application transactions)session-per-request模式不僅僅是一個(gè)可以用來(lái)設(shè)計(jì)操作單元的有用概念。很多業(yè)務(wù)處理流程都需 要一系列完整的和用戶之間的交互,即用戶對(duì)
數(shù)據(jù)庫(kù)的交叉訪問(wèn)。在基于web的應(yīng)用和企業(yè) 應(yīng)用中,跨用戶交互的
數(shù)據(jù)庫(kù)事務(wù)是無(wú)法接受的。考慮下面的例子:
在界面的第一屏,打開(kāi)對(duì)話框,用戶所看到的數(shù)據(jù)是被一個(gè)特定的 Session 和數(shù)據(jù) 庫(kù)事務(wù)載入(load)的。用戶可以隨意修改對(duì)話框中的數(shù)據(jù)對(duì)象。
5分鐘后,用戶點(diǎn)擊“保存”,期望所做出的修改被持久化;同時(shí)他也期望自己是唯一修改這個(gè)信息的人,不會(huì)出現(xiàn) 修改沖突。
從用戶的角度來(lái)看,我們把這個(gè)操作單元稱為應(yīng)用程序長(zhǎng)事務(wù)(application transaction)。 在你的應(yīng)用程序中,可以有很多種方法來(lái)實(shí)現(xiàn)它。
頭一個(gè)幼稚的做法是,在用戶思考的過(guò)程中,保持Session和
數(shù)據(jù)庫(kù)事務(wù)是打開(kāi)的, 保持
數(shù)據(jù)庫(kù)鎖定,以阻止并發(fā)修改,從而保證
數(shù)據(jù)庫(kù)事務(wù)隔離級(jí)別和原子操作。這種方式當(dāng)然是一個(gè)反模式, 因?yàn)?a target="_blank">數(shù)據(jù)庫(kù)鎖定的維持會(huì)導(dǎo)致應(yīng)用程序無(wú)法擴(kuò)展并發(fā)用戶的數(shù)目。
很明顯,我們必須使用多個(gè)
數(shù)據(jù)庫(kù)事務(wù)來(lái)實(shí)現(xiàn)一個(gè)應(yīng)用程序事務(wù)。在這個(gè)例子中,維護(hù)業(yè)務(wù)處理流程的 事務(wù)隔離變成了應(yīng)用程序?qū)拥牟糠重?zé)任。單個(gè)應(yīng)用程序事務(wù)通常跨越多個(gè)
數(shù)據(jù)庫(kù)事務(wù)。如果僅僅只有一 個(gè)
數(shù)據(jù)庫(kù)事務(wù)(最后的那個(gè)事務(wù))保存更新過(guò)的數(shù)據(jù),而所有其他事務(wù)只是單純的讀取數(shù)據(jù)(例如在一 個(gè)跨越多個(gè)請(qǐng)求/響應(yīng)周期的向?qū)эL(fēng)格的對(duì)話框中),那么應(yīng)用程序事務(wù)將保證其原子性。這種方式比聽(tīng) 起來(lái)還要容易實(shí)現(xiàn),特別是當(dāng)你使用了Hibernate的下述特性的時(shí)候:
自動(dòng)版本化 - Hibernate能夠自動(dòng)進(jìn)行樂(lè)觀并發(fā)控制 ,如果在用戶思考 的過(guò)程中發(fā)生并發(fā)修改沖突,Hibernate能夠自動(dòng)檢測(cè)到。
脫管對(duì)象(Detached Objects)- 如果你決定采用前面已經(jīng)討論過(guò)的 session-per-request模式,所有載入的實(shí)例在用戶思考的過(guò)程中都處于與Session脫離的狀態(tài)。Hibernate允許你把與Session脫離的對(duì)象重新關(guān)聯(lián)到Session 上,并且對(duì)修改進(jìn)行持久化,這種模式被稱為 session-per-request-with-detached-objects。自動(dòng)版本化被用來(lái)隔離并發(fā)修改。
長(zhǎng)生命周期的Session (Long Session)- Hibernate 的Session 可以在
數(shù)據(jù)庫(kù)事務(wù)提交之后和底層的JDBC連接斷開(kāi),當(dāng)一個(gè)新的客戶端請(qǐng)求到來(lái)的時(shí)候,它又重新連接上底層的 JDBC連接。這種模式被稱之為session-per-application-transaction,這種情況可能會(huì)造成不必要的Session和JDBC連接的重新關(guān)聯(lián)。自動(dòng)版本化被用來(lái)隔離并發(fā)修改。
session-per-request-with-detached-objects 和 session-per-application-transaction 各有優(yōu)缺點(diǎn),我們?cè)诒菊潞竺鏄?lè)觀并發(fā) 控制那部分再進(jìn)行討論。
12.1.3.關(guān)注對(duì)象標(biāo)識(shí)(Considering object identity)應(yīng)用程序可能在兩個(gè)不同的Session中并發(fā)訪問(wèn)同一持久化狀態(tài),但是, 一個(gè)持久化類的實(shí)例無(wú)法在兩個(gè) Session中共享。因此有兩種不同的標(biāo)識(shí)語(yǔ)義:
數(shù)據(jù)庫(kù)標(biāo)識(shí)
foo.getId().equals( bar.getId() )
JVM 標(biāo)識(shí)
foo==bar
對(duì)于那些關(guān)聯(lián)到 特定Session (也就是在單個(gè)Session的范圍內(nèi))上的對(duì)象來(lái)說(shuō),這 兩種標(biāo)識(shí)的語(yǔ)義是等價(jià)的,與
數(shù)據(jù)庫(kù)標(biāo)識(shí)對(duì)應(yīng)的JVM標(biāo)識(shí)是由Hibernate來(lái)保 證的。不過(guò),當(dāng)應(yīng)用程序在兩個(gè)不同的session中并發(fā)訪問(wèn)具有同一持久化標(biāo)識(shí)的業(yè)務(wù)對(duì)象實(shí)例的時(shí)候,這個(gè)業(yè)務(wù)對(duì)象的兩個(gè)實(shí)例事實(shí)上是不相同的(從 JVM識(shí)別來(lái)看)。這種沖突可以通過(guò)在同步和提交的時(shí)候使用自動(dòng)版本化和樂(lè)觀鎖定方法來(lái)解決。
這種方式把關(guān)于并發(fā)的頭疼問(wèn)題留給了Hibernate和
數(shù)據(jù)庫(kù);由于在單個(gè)線程內(nèi),操作單元中的對(duì)象識(shí)別不 需要代價(jià)昂貴的鎖定或其他意義上的同步,因此它同時(shí)可以提供最好的可伸縮性。只要在單個(gè)線程只持有一個(gè) Session,應(yīng)用程序就不需要同步任何業(yè)務(wù)對(duì)象。在Session 的范圍內(nèi),應(yīng)用程序可以放心的使用==進(jìn)行對(duì)象比較。
不過(guò),應(yīng)用程序在Session的外面使用==進(jìn)行對(duì)象比較可能會(huì) 導(dǎo)致無(wú)法預(yù)期的結(jié)果。在一些無(wú)法預(yù)料的場(chǎng)合,例如,如果你把兩個(gè)脫管對(duì)象實(shí)例放進(jìn)同一個(gè) Set的時(shí)候,就可能發(fā)生。這兩個(gè)對(duì)象實(shí)例可能有同一個(gè)
數(shù)據(jù)庫(kù)標(biāo)識(shí)(也就是說(shuō), 他們代表了表的同一行數(shù)據(jù)),從JVM標(biāo)識(shí)的定義上來(lái)說(shuō),對(duì)脫管的對(duì)象而言,Hibernate無(wú)法保證他們的的JVM標(biāo)識(shí)一致。開(kāi)發(fā)人員必須覆蓋持久化類的equals()方法和 hashCode() 方法,從而實(shí)現(xiàn)自定義的對(duì)象相等語(yǔ)義。警告:不要使用
數(shù)據(jù)庫(kù)標(biāo)識(shí) 來(lái)實(shí)現(xiàn)對(duì)象相等,應(yīng)該使用業(yè)務(wù)鍵值,由唯一的,通常不變的屬性組成。當(dāng)一個(gè)瞬時(shí)對(duì)象被持久化的時(shí) 候,它的
數(shù)據(jù)庫(kù)標(biāo)識(shí)會(huì)發(fā)生改變。如果一個(gè)瞬時(shí)對(duì)象(通常也包括脫管對(duì)象實(shí)例)被放入一 個(gè)Set,改變它的hashcode會(huì)導(dǎo)致與這個(gè)Set的關(guān)系中斷。雖 然業(yè)務(wù)鍵值的屬性不象
數(shù)據(jù)庫(kù)主鍵那樣穩(wěn)定不變,但是你只需要保證在同一個(gè)Set 中的對(duì)象屬性的穩(wěn)定性就足夠了。請(qǐng)到Hibernate
網(wǎng)站去尋求這個(gè)問(wèn)題更多的詳細(xì)的討論。請(qǐng)注意,這不是一 個(gè)有關(guān)Hibernate的問(wèn)題,而僅僅是一個(gè)關(guān)于Java對(duì)象標(biāo)識(shí)和判等行為如何實(shí)現(xiàn)的問(wèn)題。
12.1.4.常見(jiàn)問(wèn)題決不要使用反模式session-per-user-session或者 session-per-application(當(dāng)然,這個(gè)規(guī)定幾乎沒(méi)有例外)。請(qǐng)注意,下述一些問(wèn)題可能也會(huì)出現(xiàn)在我們推薦的模式中,在你作出某個(gè)設(shè)計(jì)決定之前,請(qǐng)務(wù)必理解該模式的應(yīng)用前提。
Session 是一個(gè)非線程
安全的類。如果一個(gè)Session 實(shí)例允許共享的話,那些支持并發(fā)運(yùn)行的東東,例如HTTP request,session beans,或者是 Swing workers,將會(huì)導(dǎo)致出現(xiàn)資源爭(zhēng)用(race condition)。如果在HttpSession中有 Hibernate 的Session的話(稍后討論),你應(yīng)該考慮同步訪問(wèn)你的Http session。否則,只要用戶足夠快的點(diǎn)擊瀏覽器的“刷新”,就會(huì)導(dǎo)致兩個(gè)并發(fā)運(yùn)行線程使用同一個(gè) Session。
一個(gè)由Hibernate拋出的異常意味著你必須立即回滾
數(shù)據(jù)庫(kù)事務(wù),并立即關(guān)閉Session (稍后會(huì)展開(kāi)討論)。如果你的Session綁定到一個(gè)應(yīng)用程序上,你必 須停止該應(yīng)用程序。回滾
數(shù)據(jù)庫(kù)事務(wù)并不會(huì)把你的業(yè)務(wù)對(duì)象退回到事務(wù)啟動(dòng)時(shí)候的狀態(tài)。這 意味著
數(shù)據(jù)庫(kù)狀態(tài)和業(yè)務(wù)對(duì)象狀態(tài)不同步。通常情況下,這不是什么問(wèn)題,因?yàn)楫惓J遣豢?恢復(fù)的,你必須在回滾之后重新開(kāi)始執(zhí)行。
Session 緩存了處于持久化狀態(tài)的每個(gè)對(duì)象(Hibernate會(huì)監(jiān)視和檢查臟數(shù)據(jù))。這意味著,如果你讓Session打開(kāi)很長(zhǎng)一段時(shí)間,或是僅僅載入了過(guò)多的數(shù)據(jù), Session占用的內(nèi)存會(huì)一直增長(zhǎng),直到拋出OutOfMemoryException異常。這個(gè) 問(wèn)題的一個(gè)解決方法是調(diào)用clear() 和evict()來(lái)管理 Session的緩存,但是如果你需要大批量數(shù)據(jù)操作的話,最好考慮 使用存儲(chǔ)過(guò)程。在第14章 批量處理(Batch processing)中有一些解決方案。在用戶會(huì)話期間一直保持 Session打開(kāi)也意味著出現(xiàn)臟數(shù)據(jù)的可能性很高。
12.2.數(shù)據(jù)庫(kù)事務(wù)聲明數(shù)據(jù)庫(kù)(或者系統(tǒng))事務(wù)的聲明總是必須的。在
數(shù)據(jù)庫(kù)事務(wù)之外,就無(wú)法和
數(shù)據(jù)庫(kù)通訊(這可能會(huì)讓那些習(xí)慣于 自動(dòng)提交事務(wù)模式的開(kāi)發(fā)人員感到迷惑)。永遠(yuǎn)使用清晰的事務(wù)聲明,即使只讀操作也是如此。進(jìn)行 顯式的事務(wù)聲明并不總是需要的,這取決于你的事務(wù)隔離級(jí)別和
數(shù)據(jù)庫(kù)的能力,但不管怎么說(shuō),聲明事務(wù)總歸有益無(wú)害。
一個(gè)Hibernate應(yīng)用程序可以運(yùn)行在非托管環(huán)境中(也就是獨(dú)立運(yùn)行的應(yīng)用程序,簡(jiǎn)單Web應(yīng)用程序, 或者Swing圖形桌面應(yīng)用程序),也可以運(yùn)行在托管的J2EE環(huán)境中。在一個(gè)非托管環(huán)境中,Hibernate 通常自己負(fù)責(zé)管理
數(shù)據(jù)庫(kù)連接池。應(yīng)用程序開(kāi)發(fā)人員必須手工設(shè)置事務(wù)聲明,換句話說(shuō),就是手工啟 動(dòng),提交,或者回滾
數(shù)據(jù)庫(kù)事務(wù)。一個(gè)托管的環(huán)境通常提供了容器管理事務(wù),例如事務(wù)裝配通過(guò)可聲 明的方式定義在EJB session beans的部署描述符中。可編程式事務(wù)聲明不再需要,即使是 Session 的同步也可以自動(dòng)完成。
讓持久層具備可移植性是人們的理想。Hibernate提供了一套稱為T(mén)ransaction的封裝API,用來(lái)把你的部署環(huán)境中的本地事務(wù)管理系統(tǒng)轉(zhuǎn)換到Hibernate事務(wù)上。這個(gè)API是可選的,但是我們強(qiáng)烈 推薦你使用,除非你用CMT session bean。
通常情況下,結(jié)束 Session 包含了四個(gè)不同的階段:
同步session(flush,刷出到磁盤(pán))
提交事務(wù)
關(guān)閉session
處理異常
session的同步(flush,刷出)前面已經(jīng)討論過(guò)了,我們現(xiàn)在進(jìn)一步考察在托管和非托管環(huán)境下的事務(wù)聲明和異常處理。
12.2.1.非托管環(huán)境如果Hibernat持久層運(yùn)行在一個(gè)非托管環(huán)境中,
數(shù)據(jù)庫(kù)連接通常由Hibernate的連接池機(jī)制 來(lái)處理。
代碼內(nèi)容 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 - 對(duì)commit()的調(diào)用會(huì)自動(dòng)觸發(fā)session的同步。
調(diào)用 close() 標(biāo)志session的結(jié)束。 close()方法重要的暗示是,session釋放了JDBC連接。
這段Java代碼是可移植的,可以在非托管環(huán)境和JTA環(huán)境中運(yùn)行。
你很可能從未在一個(gè)標(biāo)準(zhǔn)的應(yīng)用程序的業(yè)務(wù)代碼中見(jiàn)過(guò)這樣的用法;致命的(系統(tǒng))異常應(yīng)該總是在應(yīng)用程序“頂層”被捕獲。換句話說(shuō),執(zhí)行Hibernate調(diào)用的代碼(在持久層)和處理 RuntimeException異常的代碼(通常只能清理和退出應(yīng)用程序)應(yīng)該在不同的應(yīng)用程序邏輯層。這對(duì)于你設(shè)計(jì)自己的軟件系統(tǒng)來(lái)說(shuō)是一個(gè)挑戰(zhàn),只要有可能,你就應(yīng)該使用 J2EE/EJB容器服務(wù)。異常處理將在本章稍后進(jìn)行討論。
請(qǐng)注意,你應(yīng)該選擇 org.hibernate.transaction.JDBCTransactionFactory (這是默認(rèn)選項(xiàng)).
12.2.2.使用JTA如果你的持久層運(yùn)行在一個(gè)應(yīng)用服務(wù)器中(例如,在EJB session beans的后面),Hibernate獲取 的每個(gè)數(shù)據(jù)源連接將自動(dòng)成為全局JTA事務(wù)的一部分。Hibernate提供了兩種策略進(jìn)行JTA集成。
如果你使用bean管理事務(wù)(BMT),可以通過(guò)使用Hibernate的 Transaction API來(lái)告訴 應(yīng)用服務(wù)器啟動(dòng)和結(jié)束B(niǎo)MT事務(wù)。因此,事務(wù)管理代碼和在非托管環(huán)境下是一樣的。
代碼內(nèi)容 // 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 方式下,事務(wù)聲明是在session bean的部署描述符中,而不需要編程。除非你設(shè)置了屬性hibernate.transaction.flush_before_completion和 hibernate.transaction.auto_close_session為true,否則你必須自己同步和關(guān)閉Session。Hibernate可以為你自動(dòng)同步和關(guān)閉 Session。你唯一要做的就是當(dāng)發(fā)生異常時(shí)進(jìn)行事務(wù)回滾。幸運(yùn)的是, 在一個(gè)CMT bean中,事務(wù)回滾甚至可以由容器自動(dòng)進(jìn)行,因?yàn)橛蓅ession bean方法拋出的未處理的 RuntimeException異常可以通知容器設(shè)置全局事務(wù)回滾。這意味著在CMT中,你完全無(wú)需使用Hibernate的Transaction API 。
請(qǐng)注意,當(dāng)你配置Hibernate事務(wù)工廠的時(shí)候,在一個(gè)BMT session bean中,你應(yīng)該選擇 org.hibernate.transaction.JTATransactionFactory,在一個(gè) CMT session bean中選擇org.hibernate.transaction.CMTTransactionFactory。記住,同時(shí)也要設(shè)置org.hibernate.transaction.manager_lookup_class。
如果你使用CMT環(huán)境,并且讓容器自動(dòng)同步和關(guān)閉session,你可能也希望在你代碼的不同部分使用同一個(gè)session。一般來(lái)說(shuō),在一個(gè)非托管環(huán)境中,你可以使用一個(gè)ThreadLocal 變量來(lái)持有這個(gè)session,但是單個(gè)EJB方法調(diào)用可能會(huì)在不同的線程中執(zhí)行(舉例來(lái)說(shuō),一個(gè)session bean調(diào)用另一個(gè)session bean)。如果你不想在應(yīng)用代碼中被傳遞Session對(duì) 象實(shí)例的問(wèn)題困擾的話,那么SessionFactory 提供的 getCurrentSession()方法就很適合你,該方法返回一個(gè)綁定到JTA事務(wù)上下文環(huán)境中的session實(shí)例。這也是把Hibernate集成到一個(gè)應(yīng)用程序中的最簡(jiǎn)單的方法!這個(gè)“當(dāng)前的”session總是可以自動(dòng)同步和自動(dòng)關(guān)閉(不考慮上述的屬性設(shè)置)。我們的session/transaction 管理代碼減少到如下所示:
代碼內(nèi)容 // CMT idiom Session sess = factory.getCurrentSession();
// do some work ... |
換句話來(lái)說(shuō),在一個(gè)托管環(huán)境下,你要做的所有的事情就是調(diào)用 SessionFactory.getCurrentSession(),然后進(jìn)行你的數(shù)據(jù)訪問(wèn),把其余的工作交給容器來(lái)做。事務(wù)在你的session bean的部署描述符中以可聲明的方式來(lái)設(shè)置。session的生命周期完全 由Hibernate來(lái)管理。
對(duì)after_statement連接釋放方式有一個(gè)警告。因?yàn)镴TA規(guī)范的一個(gè)很愚蠢的限制,Hibernate不可能自動(dòng)清理任何未關(guān)閉的 ScrollableResults 或者Iterator,它們是由scroll()或iterate()產(chǎn)生的。你must通過(guò)在finally塊中,顯式調(diào)用 ScrollableResults.close()或者Hibernate.close(Iterator)方法來(lái)釋放底層
數(shù)據(jù)庫(kù)游標(biāo)。(當(dāng)然,大部分程序完全可以很容易的避免在CMT代碼中出現(xiàn)scroll()或iterate()。)
12.2.3.異常處理如果 Session 拋出異常 (包括任何SQLException), 你應(yīng)該立即回滾
數(shù)據(jù)庫(kù)事務(wù),調(diào)用 Session.close() ,丟棄該 Session實(shí)例。Session的某些方法可能會(huì)導(dǎo)致session 處于不一致的狀態(tài)。所有由Hibernate拋出的異常都視為不可以恢復(fù)的。確保在 finally 代碼塊中調(diào)用close()方法,以關(guān)閉掉 Session。
HibernateException是一個(gè)非檢查期異常(這不同于Hibernate老的版本),它封裝了Hibernate持久層可能出現(xiàn)的大多數(shù)錯(cuò)誤。我們的觀點(diǎn)是,不應(yīng)該強(qiáng)迫應(yīng)用程序開(kāi)發(fā)人員在底層捕獲無(wú)法恢復(fù)的異常。在大多數(shù)軟件系統(tǒng)中,非檢查期異常和致命異常都是在相應(yīng)方法調(diào)用的堆棧的頂層被處理的(也就是說(shuō),在軟件上面的邏輯層),并且提供一個(gè)錯(cuò)誤信息給應(yīng)用軟件的用戶(或者采取其他某些相應(yīng)的操作)。請(qǐng)注意,Hibernate也有可能拋出其他并不屬于 HibernateException的非檢查期異常。這些異常同樣也是無(wú)法恢復(fù)的,應(yīng)該 采取某些相應(yīng)的操作去處理。
在和
數(shù)據(jù)庫(kù)進(jìn)行交互時(shí),Hibernate把捕獲的SQLException封裝為Hibernate的 JDBCException。事實(shí)上,Hibernate嘗試把異常轉(zhuǎn)換為更有實(shí)際含義的JDBCException異常的子類。底層的SQLException可以通過(guò)JDBCException.getCause()來(lái)得到。Hibernate通過(guò)使用關(guān)聯(lián)到 SessionFactory上的SQLExceptionConverter來(lái)把SQLException轉(zhuǎn)換為一個(gè)對(duì)應(yīng)的JDBCException 異常的子類。默認(rèn)情況下,SQLExceptionConverter可以通過(guò)配置dialect 選項(xiàng)指定;此外,也可以使用用戶自定義的實(shí)現(xiàn)類(參考javadocs SQLExceptionConverterFactory類來(lái)了解詳情)。標(biāo)準(zhǔn)的 JDBCException子類型是:
JDBCConnectionException - 指明底層的JDBC通訊出現(xiàn)錯(cuò)誤
SQLGrammarException - 指明發(fā)送的SQL語(yǔ)句的語(yǔ)法或者格式錯(cuò)誤
ConstraintViolationException - 指明某種類型的約束違例錯(cuò)誤
LockAcquisitionException - 指明了在執(zhí)行請(qǐng)求操作時(shí),獲取 所需的鎖級(jí)別時(shí)出現(xiàn)的錯(cuò)誤。
GenericJDBCException - 不屬于任何其他種類的原生異常
12.3.樂(lè)觀并發(fā)控制(Optimistic concurrency control)唯一能夠同時(shí)保持高并發(fā)和高可伸縮性的方法就是使用帶版本化的樂(lè)觀并發(fā)控制。版本檢查使用版本號(hào)、或者時(shí)間戳來(lái)檢測(cè)更新沖突(并且防止更新丟失)。Hibernate為使用樂(lè)觀并發(fā)控制的代碼提供了三種可能的方法,應(yīng)用程序在編寫(xiě)這些代碼時(shí),可以采用它們。我們已經(jīng)在前面應(yīng)用程序長(zhǎng)事務(wù)那部分展示了 樂(lè)觀并發(fā)控制的應(yīng)用場(chǎng)景,此外,在單個(gè)
數(shù)據(jù)庫(kù)事務(wù)范圍內(nèi),版本檢查也提供了防止更新丟失的好處。
12.3.1.應(yīng)用程序級(jí)別的版本檢查(Application version checking)
未能充分利用Hibernate功能的實(shí)現(xiàn)代碼中,每次和
數(shù)據(jù)庫(kù)交互都需要一個(gè)新的 Session,而且開(kāi)發(fā)人員必須在顯示數(shù)據(jù)之前從
數(shù)據(jù)庫(kù)中重 新載入所有的持久化對(duì)象實(shí)例。這種方式迫使應(yīng)用程序自己實(shí)現(xiàn)版本檢查來(lái)確保 應(yīng)用程序事務(wù)的隔離,從數(shù)據(jù)訪問(wèn)的角度來(lái)說(shuō)是最低效的。這種使用方式和 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 屬性使用 來(lái)映射,如果對(duì)象 是臟數(shù)據(jù),在同步的時(shí)候,Hibernate會(huì)自動(dòng)增加版本號(hào)。
當(dāng)然,如果你的應(yīng)用是在一個(gè)低數(shù)據(jù)并發(fā)環(huán)境下,并不需要版本檢查的話,你照樣可以使用 這種方式,只不過(guò)跳過(guò)版本檢查就是了。在這種情況下,最晚提交生效(last commit wins)就是你的應(yīng)用程序長(zhǎng)事務(wù)的默認(rèn)處理策略。請(qǐng)記住這種策略可能會(huì)讓?xiě)?yīng)用軟件的用戶感到困惑,因?yàn)樗麄冇锌赡軙?huì)碰上更新丟失掉卻沒(méi) 有出錯(cuò)信息,或者需要合并更改沖突的情況。
很明顯,手工進(jìn)行版本檢查只適合于某些軟件規(guī)模非常小的應(yīng)用場(chǎng)景,對(duì)于大多數(shù)軟件應(yīng)用場(chǎng)景來(lái)說(shuō)并不現(xiàn)實(shí)。通常情況下,不僅是單個(gè)對(duì)象實(shí)例需要進(jìn)行版本檢查,整個(gè)被修改過(guò)的關(guān)聯(lián)對(duì)象圖也都需要進(jìn)行版本檢查。作為標(biāo)準(zhǔn)設(shè)計(jì)范例,Hibernate使用長(zhǎng)生命周期 Session的方式,或者脫管對(duì)象實(shí)例的方式來(lái)提供自動(dòng)版本檢查。
12.3.2.長(zhǎng)生命周期session和自動(dòng)版本化 單個(gè) Session實(shí)例和它所關(guān)聯(lián)的所有持久化對(duì)象實(shí)例都被用于整個(gè)應(yīng)用程序事務(wù)。Hibernate在同步的時(shí)候進(jìn)行對(duì)象實(shí)例的版本檢查,如果檢測(cè)到并發(fā)修改則拋出異常。由開(kāi)發(fā)人員來(lái)決定是否需要捕獲和處理這個(gè)異常(通常的抉擇是給用戶 提供一個(gè)合并更改,或者在無(wú)臟數(shù)據(jù)情況下重新進(jìn)行業(yè)務(wù)操作的機(jī)會(huì))。
在等待用戶交互的時(shí)候, Session 斷開(kāi)底層的JDBC連接。這種方式 以
數(shù)據(jù)庫(kù)訪問(wèn)的角度來(lái)說(shuō)是最高效的方式。應(yīng)用程序不需要關(guān)心版本檢查或脫管對(duì)象實(shí)例 的重新關(guān)聯(lián),在每個(gè)
數(shù)據(jù)庫(kù)事務(wù)中,應(yīng)用程序也不需要載入讀取對(duì)象實(shí)例。
代碼內(nèi)容 // 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 對(duì)象始終和載入它的Session相關(guān)聯(lián)。 Session.reconnect()獲取一個(gè)新的
數(shù)據(jù)庫(kù)連接(或者 你可以提供一個(gè)),并且繼續(xù)當(dāng)前的session。Session.disconnect() 方法把session與JDBC連接斷開(kāi),把
數(shù)據(jù)庫(kù)連接返回到連接池(除非是你自己提供的數(shù)據(jù) 庫(kù)連接)。在Session重新連接上
數(shù)據(jù)庫(kù)連接之后,你可以對(duì)任何可能被其他事務(wù)更新過(guò) 的對(duì)象調(diào)用Session.lock(),設(shè)置LockMode.READ 鎖定模式,這樣你就可以對(duì)那些你不準(zhǔn)備更新的數(shù)據(jù)進(jìn)行強(qiáng)制版本檢查。此外,你并不需要 鎖定那些你準(zhǔn)備更新的數(shù)據(jù)。
假若對(duì)disconnect()和reconnect()的顯式調(diào)用發(fā)生得太頻繁了,你可以使用hibernate.connection.release_mode來(lái)代替。
如果在用戶思考的過(guò)程中,Session因?yàn)樘罅硕荒鼙4妫敲催@種模式是有 問(wèn)題的。舉例來(lái)說(shuō),一個(gè)HttpSession應(yīng)該盡可能的小。由于 Session是一級(jí)緩存,并且保持了所有被載入過(guò)的對(duì)象,因此我們只應(yīng)該在那些少量的request/response情況下使用這種策略。而且在這種情況下, Session 里面很快就會(huì)有臟數(shù)據(jù)出現(xiàn),因此請(qǐng)牢牢記住這一建議。
此外,也請(qǐng)注意,你應(yīng)該讓與
數(shù)據(jù)庫(kù)連接斷開(kāi)的Session對(duì)持久層保持 關(guān)閉狀態(tài)。換句話說(shuō),使用有狀態(tài)的EJB session bean來(lái)持有Session, 而不要把它傳遞到web層(甚至把它序列化到一個(gè)單獨(dú)的層),保存在HttpSession中。
12.3.3.脫管對(duì)象(deatched object)和自動(dòng)版本化 這種方式下,與持久化存儲(chǔ)的每次交互都發(fā)生在一個(gè)新的Session中。 然而,同一持久化對(duì)象實(shí)例可以在多次與
數(shù)據(jù)庫(kù)的交互中重用。應(yīng)用程序操縱脫管對(duì)象實(shí)例 的狀態(tài),這個(gè)脫管對(duì)象實(shí)例最初是在另一個(gè)Session 中載入的,然后 調(diào)用 Session.update(),Session.saveOrUpdate(), 或者 Session.merge() 來(lái)重新關(guān)聯(lián)該對(duì)象實(shí)例。
代碼內(nèi)容 // 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會(huì)再一次在同步的時(shí)候檢查對(duì)象實(shí)例的版本,如果發(fā)生更新沖突,就拋出異常。
如果你確信對(duì)象沒(méi)有被修改過(guò),你也可以調(diào)用lock() 來(lái)設(shè)置 LockMode.READ(繞過(guò)所有的緩存,執(zhí)行版本檢查),從而取 代 update()操作。
12.3.4.定制自動(dòng)版本化行為對(duì)于特定的屬性和集合,通過(guò)為它們?cè)O(shè)置映射屬性optimistic-lock的值 為false,來(lái)禁止Hibernate的版本自動(dòng)增加。這樣的話,如果該屬性 臟數(shù)據(jù),Hibernate將不再增加版本號(hào)。
遺留系統(tǒng)的
數(shù)據(jù)庫(kù)Schema通常是靜態(tài)的,不可修改的。或者,其他應(yīng)用程序也可能訪問(wèn)同一數(shù)據(jù) 庫(kù),根本無(wú)法得知如何處理版本號(hào),甚至?xí)r間戳。在以上的所有場(chǎng)景中,實(shí)現(xiàn)版本化不能依靠
數(shù)據(jù)庫(kù)表的某個(gè)特定列。在的映射中設(shè)置 optimistic-lock="all"可以在沒(méi)有版本或者時(shí)間戳屬性映射的情況下實(shí)現(xiàn)版本檢查,此時(shí)Hibernate將比較一行記錄的每個(gè)字段的狀態(tài)。請(qǐng)注意,只有當(dāng)Hibernate能夠比較新舊狀態(tài)的情況下,這種方式才能生效,也就是說(shuō), 你必須使用單個(gè)長(zhǎng)生命周期Session模式,而不能使用 session-per-request-with-detached-objects模式。
有些情況下,只要更改不發(fā)生交錯(cuò),并發(fā)修改也是允許的。當(dāng)你在 的映射中設(shè)置optimistic-lock="dirty",Hibernate在同步的時(shí)候?qū)⒅槐容^有臟 數(shù)據(jù)的字段。
在以上所有場(chǎng)景中,不管是專門(mén)設(shè)置一個(gè)版本/時(shí)間戳列,還是進(jìn)行全部字段/臟數(shù)據(jù)字段比較, Hibernate都會(huì)針對(duì)每個(gè)實(shí)體對(duì)象發(fā)送一條UPDATE(帶有相應(yīng)的 WHERE語(yǔ)句)的SQL語(yǔ)句來(lái)執(zhí)行版本檢查和數(shù)據(jù)更新。如果你對(duì)關(guān)聯(lián)實(shí)體 設(shè)置級(jí)聯(lián)關(guān)系使用傳播性持久化(transitive persistence),那么Hibernate可能會(huì)執(zhí)行不必 要的update語(yǔ)句。這通常不是個(gè)問(wèn)題,但是
數(shù)據(jù)庫(kù)里面對(duì)on update點(diǎn)火 的觸發(fā)器可能在脫管對(duì)象沒(méi)有任何更改的情況下被觸發(fā)。因此,你可以在的映射中,通過(guò)設(shè)置select-before-update="true" 來(lái)定制這一行為,強(qiáng)制Hibernate SELECT這個(gè)對(duì)象實(shí)例,從而保證, 在更新記錄之前,對(duì)象的確是被修改過(guò)。
12.4.悲觀鎖定(Pessimistic Locking)用戶其實(shí)并不需要花很多精力去擔(dān)心鎖定策略的問(wèn)題。通常情況下,只要為JDBC連接指定一下隔 離級(jí)別,然后讓
數(shù)據(jù)庫(kù)去搞定一切就夠了。然而,高級(jí)用戶有時(shí)候希望進(jìn)行一個(gè)排它的悲觀鎖定, 或者在一個(gè)新的事務(wù)啟動(dòng)的時(shí)候,重新進(jìn)行鎖定。
Hibernate總是使用
數(shù)據(jù)庫(kù)的鎖定機(jī)制,從不在內(nèi)存中鎖定對(duì)象!
類LockMode 定義了Hibernate所需的不同的鎖定級(jí)別。一個(gè)鎖定 可以通過(guò)以下的機(jī)制來(lái)設(shè)置:
當(dāng)Hibernate更新或者插入一行記錄的時(shí)候,鎖定級(jí)別自動(dòng)設(shè)置為L(zhǎng)ockMode.WRITE。
當(dāng)用戶顯式的使用
數(shù)據(jù)庫(kù)支持的SQL格式SELECT ... FOR UPDATE 發(fā)送SQL的時(shí)候,鎖定級(jí)別設(shè)置為L(zhǎng)ockMode.UPGRADE
當(dāng)用戶顯式的使用Oracle
數(shù)據(jù)庫(kù)的SQL語(yǔ)句SELECT ... FOR UPDATE NOWAIT 的時(shí)候,鎖定級(jí)別設(shè)置LockMode.UPGRADE_NOWAIT
當(dāng)Hibernate在“可重復(fù)讀”或者是“序列化”
數(shù)據(jù)庫(kù)隔離級(jí)別下讀取數(shù)據(jù)的時(shí)候,鎖定模式 自動(dòng)設(shè)置為L(zhǎng)ockMode.READ。這種模式也可以通過(guò)用戶顯式指定進(jìn)行設(shè)置。
LockMode.NONE 代表無(wú)需鎖定。在Transaction結(jié)束時(shí), 所有的對(duì)象都切換到該模式上來(lái)。與session相關(guān)聯(lián)的對(duì)象通過(guò)調(diào)用update() 或者saveOrUpdate()脫離該模式。
"顯式的用戶指定"可以通過(guò)以下幾種方式之一來(lái)表示:
調(diào)用 Session.load()的時(shí)候指定鎖定模式(LockMode)。
調(diào)用Session.lock()。
調(diào)用Query.setLockMode()。
如果在UPGRADE或者UPGRADE_NOWAIT鎖定模式下調(diào)用Session.load(),并且要讀取的對(duì)象尚未被session載入過(guò),那么對(duì)象 通過(guò)SELECT ... FOR UPDATE這樣的SQL語(yǔ)句被載入。如果為一個(gè)對(duì)象調(diào)用 load()方法時(shí),該對(duì)象已經(jīng)在另一個(gè)較少限制的鎖定模式下被載入了,那么Hibernate就對(duì)該對(duì)象調(diào)用lock() 方法。
如果指定的鎖定模式是READ, UPGRADE 或 UPGRADE_NOWAIT,那么Session.lock()就 執(zhí)行版本號(hào)檢查。(在UPGRADE 或者UPGRADE_NOWAIT 鎖定模式下,執(zhí)行SELECT ... FOR UPDATE這樣的SQL語(yǔ)句。)
如果
數(shù)據(jù)庫(kù)不支持用戶設(shè)置的鎖定模式,Hibernate將使用適當(dāng)?shù)奶娲J剑ǘ皇侨映霎惓#?這一點(diǎn)可以確保應(yīng)用程序的可移植性。