事務性 Web 流的持久化策略
Spring Web Flow 2 的 JPA/Hibernate 持久化架構建立在流管理的持久化概念基礎上,這個概念過去只是簡單提到過。在本文中,Xinyu Liu 將爲您深入分析流管理的持久化的概念構建模塊和流作用域的持久化上下文。然後,他將展示在複雜的真實世界場景中處理原子和非原子 Web 流的事務性策略。
Spring Web Flow 是一種新穎的 Java™Web 框架,它擴展了 Spring MVC 技術。使用 Spring Web Flow 的應用開發圍繞着定義爲 Web 流的用例展開。 將開發工作區根據 Web 流進行組織使開發體驗更有意義、更具上下文。此外,Spring Web Flow 對 JPA/Hibernate 持久化的支持也是其最重要的服務器端改進之一。
儘管 SpringSource 和 Spring Web Flow 項目組詳細介紹了 Spring Web Flow,但是其持久化支持,尤其是其流管理的持久化機制,很少爲人所瞭解。本文將深入介紹 Spring Web Flow 2 中的 Java 持久化編程,重點講解流管理的持久化及其基本組件 —流作用域的持久化上下文。
在概述 Spring Web Flow 持久化的基本概念之後,我將呈現幾個用例,爲大家展示處理原子和非原子 Web 流中的只讀和讀 / 寫事務的策略。在每種情況下,我都將解釋首選事務處理策略的基本思想並說明其缺點。本文結尾我爲大家總結了在 Spring Web Flow 2 中高效、安全地管理事務的一些指導原則。
本文面向熟悉 Spring Web Flow 2 及其基於 continuations 的架構的經驗豐富的 Java 開發人員。已經在 Spring Web Flow 中使用 JPA/Hibernate 的開發人員將從用例和樣例應用程序代碼中受益良多。
JPA/Hibernate 中的持久化挑戰
在典型的 Web 應用程序中,有兩個主要步驟處理用戶請求:操作處理和視圖呈現。應用程序的主要業務邏輯駐留在操作處理中。隨後進行的視圖呈現將數據提供給視圖模板,將視圖展現出來。
在 JPA/Hibernate 中,數據(更確切地說是實體關係)可能急切加載或延遲加載爲代理對象。如果持久化上下文對象(JPA EntityManager
或
Hibernate Session
)在視圖呈現階段已經關閉,那麼實體就會分離。任何訪問分離實體上已卸載的關係的嘗試都將導致LazyInitializationException
異常。
Open Session in View模式(請參見 參考資料)試圖解決 LazyInitializationException
異常。當
Open Session in View 模式作爲過濾器或攔截器實現時,持久化上下文對象在視圖呈現期間會保持打開狀態。導航到持久實體上的已卸載關係將觸發其他的數據庫查詢來按需獲取關係。
Open Session in View 模式的一個缺點是持久化上下文對象被高效地劃定到用戶請求作用域內。因此,存儲在 Servlet 作用域中的實體,除當前請求外,總是被分離。分離的實體需要合併 / 重新連接 / 重新加載操作才能與當前持久化上下文關聯。
Spring Web Flow 採用了不同的方法,它通過流管理的持久化,更確切地說是流作用域的持久化上下文對象,解決了分離實體狀態的問題。
流管理的持久化
Spring Web Flow 中的應用程序開發基於 Web 流的概念,Web 流通常代表一個單獨的用例。在很多情況中,整個 Web 流中的數據變化需要是原子的,也就是說流不同階段的變化或者作爲整體被保存到後端數據庫中,或者全部取消,在數據庫中不留下任何痕跡。
Spring Web Flow 通過 流管理的持久化機制簡化了事務性原子 Web 流中的 JPA/Hibernate 編程。流管理的持久化在概念上與 Hibernate/Seam 對話一樣(請參見 參考資料),其中在 Web 流(或者 Seam 中的 “頁面流”)期間進行的數據變更都作爲髒實體緩存在同一個流作用域的持久化上下文對象中。直到流結束時纔會激活 SQL insert/update/delete 語句,將變更一次刷新並提交到數據庫中。(注意,“刷新” 和 “提交” 是不同的概念;前者激活一系列 SQL insert/update/delete 語句使髒實體與其相應數據庫值同步,而後者只是提交數據庫事務)。
流管理持久化中的 OptimisticLockingFailureException
樂觀鎖是一個極其有效的併發性控制方法,它可以確保數據完整性而又無需在數據庫中放置任何物理鎖。儘管不是強制實施的,但是強烈建議在流管理的持久化中使用樂觀鎖。
持久化上下文在刷新時檢查實體版本,如果檢查出併發的修改,它就拋出 OptimisticLockingFailureException
異常(在
Hibernate 中是StaleObjectException
異常)。實體在內存中存在的時間越長,其對應的數據庫值被其他進程修改的可能性越大。
如上所述,在 Open Session in View 模式中,實體的持久狀態受制於用戶請求。一旦實體分離,通常需要在後續用戶請求中進行合併 / 重新連接 / 重新加載操作來還原實體的持久狀態,從而使實體與其對應的數據庫值保持同步。
在流管理的持久化中,實體在多個用戶請求之間保存其持久狀態。在各用戶請求之間沒有強制執行數據庫同步,因此很有可能出現 OptimisticLockingFailureException
異常。重要的是要優雅地處理 OptimisticLockingFailureException
異常,就像處理任何檢查型業務異常一樣。(即使 OptimisticLockingFailureException
是一個回滾到數據庫事務的運行時異常,也應如此)。常用的策略是讓用戶有機會合並其變更或用未過期數據重啓流。
流作用域的持久化上下文
Web 流被聲明爲 XML 格式的流定義文件。帶有 <persistence-context/>
標籤的
Web 流啓動時,會創建一個新的持久化上下文對象並將其綁定到流作用域。在等待用戶請求時,該對象會斷開與底層 JDBC 連接的連接並在服務於用戶請求時重新連接。在整個流過程中都重用同一個持久化 - 上下文對象,這避免了分離實體狀態的問題和相應的 LazyInitializationException
異常。
持久化上下文還被綁定到當前的請求線程並以兩種方式公開給開發人員:作爲隱式變量 persistenceContext
或通過
JPA@PersistenceContext
註釋注入到任何
Spring bean 中。
隱式變量可以在流定義 XML 文件中直接得到,例如:
<evaluate expression="persistenceContext.persist(transientEntityInstance)"/>
注入的 JPA 實體管理器可以在 Spring 組件的任何地方引用,比如在 DAO 中、服務 bean 或 Web 層 bean 中。
持久化上下文的類型:事務性或擴展型
@PersistenceContext
註釋有一個可選的屬性 type
,該屬性默認爲 PersistenceContextType.TRANSACTION
(也就是綁定到事務的持久化上下文)。在用流作用域的持久化上下文編程時必須使用此默認設置。在這種情況下,注入的綁定到事務的持久化上下文對象只是一個共享的代理,它透明地委託給了綁定到線程的實際的流作用域的持久化上下文。
選擇另一個選項,也就是 PersistenceContextType.EXTENDED
,會得到一個叫做
“擴展的實體管理器” 的東西,它對線程而言是不安全的,不能在併發訪問的組件,比如單態 Spring bean,中使用。將擴展的實體管理器用作流作用域的持久化上下文會導致應用程序中出現不可預測的數據庫 / 事務行爲,因此要儘量避免使用它。
有趣地是,Seam 對話通常用注入到有狀態會話 bean (EJB) 中的擴展的實體管理器實現。這是 Spring Web Flow 的流管理的持久化和 Seam 對話之間的一個顯著區別。
流作用域的持久化上下文對象可以與 @Transactional
註釋一起使用以調整流的持久化特徵。
事務語義
作爲 Spring 核心包一部分的 @Transactional
註釋指定了註釋的類或方法的事務語義。根據
Spring 開發團隊所述,@Transactional
最好應用於具體類而不是接口。默認事務語義是:
@Transactional(readOnly=false,propagation=PROPAGATION_REQUIRED, isolation=ISOLATION_DEFAULT,timeout=TIMEOUT_DEFAULT)
readOnly:通過指定 @Transactional(readOnly=false)
建立讀
/ 寫事務,這樣會使持久化上下文的 FlushMode
變爲 AUTO
。應用@Transactional(readOnly=true)
會使底層
Hibernate 會話的 FlushMode
變爲 MANUAL
。
JPA 1.0 不支持 MANUAL
刷新以及只讀事務,因此只有在底層
JPA 提供商,比如 Hibernate,支持只讀數據庫事務時,@Transactional(readOnly=true)
纔有意義。而且,Hibernate
將此設置用作針對某些數據庫類型的數據庫提示從而優化查詢性能。
propagation:propagation
屬性確定當前方法是在繼承的事務下運行,
還是通過掛起 / 繼續封閉事務在新事物中運行,或者根本沒有在事務中運行。
isolation:JPA 1.0 不支持自定義隔離級別,因此開發人員需要指定數據庫端的默認事務隔離級別。Read-Committed
是樂觀鎖工作所需的最低級別。
timeout:timeout
屬性指定在超時(以及被底層事務基礎設施自動回滾)之前事務可以運行多長時間
。
rollbackFor、rollbackForClassname、noRollbackFor、noRollbackForClassname: 一般而言,在出現表示系統錯誤的RuntimeException
異常時事務總是回滾,在遇到帶有預定義業務意義的檢查型 Exception
時總是會提交。可以通過這
4 個回滾屬性自定義默認語義。
Spring 核心包的健壯的事務基礎設施使絕大部分真實開發場景中的事務管理更加輕鬆。在下面各節中,我們將瞭解 Spring Web Flow 如何利用 Spring 事務基礎設施連同其自己的流作用域的持久化上下文對象來處理各種 Web 流中的持久化編程,包括一些展示了流管理持久化的侷限性的用例。
原子 Web 流
流管理持久化旨在處理那些從事務角度來說屬於原子性的 Spring Web Flow 用例。例如,假設有一個網銀系統,該系統允許用戶將資金從支票賬戶移動到儲蓄賬戶或即將建立的定期賬戶。事務必須分幾個步驟完成:
- 用戶選擇要轉賬的支票賬戶。
- 系統顯示賬戶餘額。
- 用戶輸入要轉賬的金額。
- 用戶選擇一個儲蓄或定期賬戶作爲目標。
- 系統顯示整個交易的摘要供用戶檢查。
- 用戶決定提交或取消交易。
由於明顯的併發性需求,您應該首先在實體類上啓用樂觀鎖。爲此,您可以使用 JPA @Version
註釋或者
Hibernate 私有OptimisticLockType.ALL
屬性。然後將整個用例映射到一個帶有
Spring Web Flow 的 <persistence-context/>
標籤的
Web 流中。
Web 流中非事務性數據訪問
在 Spring Web Flow 中,默認情況下,所有數據訪問都是非事務性的。對於非事務性數據訪問,Hibernate 將底層數據庫的 auto-commit
模式設置爲 true
,這樣每個
SQL 語句都會在其自己的 “短事務” 中立即執行,提交或回滾。從應用程序的角度而言,數據庫短事務等效於根本沒有事務。更爲嚴重的是,對於非事務性操作,Hibernate 禁用了默認的 FlushMode.AUTO
。它在 FlushMode.MANUAL
模式下高效地工作。
禁用 FlushMode.AUTO
對於流管理的持久化而言很重要。視圖呈現階段的實體延遲讀取也在非事務性模式下執行。如果在呈現不同視圖期間發生過刷新,那麼就無法在流末尾完成延遲的刷新。在本質上,auto-commit
模式中的非事務性讀取等效於隔離級別爲 Read-Committed
的事務內的讀取。類似地,非事務性寫操作永遠不會進行刷新。
在上述用例中,每個用戶操作都可以在數據庫事務之外執行,無需指定 @Transactional
註釋或 XML 配置的事務顧問。 流作用域的持久化上下文對象將流期間加載的數據作爲持久化實體來管理並將數據變更緩存爲實體的髒狀態。
如果用戶在流末尾通過 <end-state
commit="true"/>
確認了轉賬交易,那麼 Spring Web Flow 運行時將在讀 / 寫數據庫事務內隱式地調用entityManager.flush()
。然後提交事務,取消綁定持久化上下文並關閉它。如果用戶選擇通過 <end-state
commit="false"/>
取消了交易,那麼所有緩存的數據變更都會在關閉流作用域的持久化上下文之際在內存中被丟棄。
流管理持久化使用的這種方法與 JPA 1.0 解釋對話處理的方式是完全一致的。JpaFlowExecutionListener
類是使這一切發生的底層
Spring Web Flow 組件。除了流管理持久化的非事務性數據訪問方法之外,還可以使用只讀事務。
Web 流中的只讀事務
在某些情況下,與非事務性事務相比您可能更願意使用只讀事務。如果查看 Spring Web Flow 發行版中的樣例 “Hotel Booking” 應用程序(請參見 參考資料),會注意到在整個
“booking” Web 流期間,在全局範圍內,對所有數據訪問都使用了 @Transactional(readOnly=true)
,無論操作的本質如何(讀
/ 插入 / 更新 / 刪除)。
JPA 1.0 規範不支持只讀事務,因此只有在某些 JPA 提供商中才能使用此設置。在其 JPA 實現中,Hibernate 將底層 Hibernate 會話的FlushMode
設置爲 MANUAL
並將 auto-commit
模式設置爲 false
。
流管理持久化的只讀事務的表現與非事務性數據訪問一樣,只有在原子 Web 流結尾纔會通過 <end-state
commit="true"/>
刷新變更的實體。
如果希望刷新發生在 <end-state/>
之前,您需要在用 @Transactional
註釋的
Spring bean 方法之一中調用 entityManager.flush()
。
直接從 Web 流調用,<evaluate
expression="persistenceContext.flush()"/>
,行不通,因爲沒有事務綁定到任何 Spring Web Flow 標籤,除了 <end-state
commit="true"/>
。您會得到以下錯誤消息:
"javax.persistence.TransactionRequiredException: no transaction is in progress"
本文稍後我們會回到 “Hotel Booking” 這個示例,來了解 沒有流作用域持久化上下文的持久化編程 所面臨的挑戰。
關於事務傳播的更多信息
我已經介紹了事務如何根據其 propagation
屬性的值進行傳播,但是我忽略了一個特別的用例:如果標有 @Transactional(readOnly=true,
propagation=Propagation.REQUIRED)
的方法要調用另一個標有 @Transactional(readOnly=false,
propagation=Propagation.REQUIRED)
的方法,或者情況相反, 那麼事務又將如何傳播呢?
Spring Web Flow 用一種簡單而聰明的方式處理了這個問題:它忽略了第二個方法上的 readOnly
屬性值。簡而言之,初始化爲只讀的事務會保持只讀狀態,直到它結束,而且 反之亦然。
這對於在流管理的持久化中是不使用事務還是使用只讀事務的問題產生了有趣的影響。
只讀事務的一個用例
應用程序服務層的 Spring bean 可以通過一些 JAX-WS/JAX-RS 註釋公開爲可重用的 SOAP/REST Web 服務。在這些 @Service
bean
或其方法中應用 @Transactional
將
Web 服務調用與數據庫事務綁定到了一起(沒有明顯的原因要在 DAO @Repository
bean
上使用@Transactional
,除非應用程序具有級聯式層架構,其中沒有其他地方供開發人員指定事務屬性)。
再思考一下 Spring Web Flow 中流管理持久化的非事務性數據訪問方法。如果將 @Transactional
應用到啓用了
Web 服務的 @Service
bean,則非事務性上下文可能被覆蓋。流作用域持久化上下文中的所有未決數據變更都會在方法調用鏈中遇到在服務層指定的讀
/ 寫事務時刷新,這將導致所謂的 “提前刷新”。
另一方面,在視圖層 Spring bean 上指定 @Transactional(readOnly=true)
將覆蓋服務
bean 上的讀 / 寫事務設置;事務將保持只讀狀態以防止提前刷新。在 SOAP/REST Web 服務通信中繞過整個 Web 層的情況下,應用到服務 bean 的 @Transactional
註釋確保
Web 服務調用運行在數據庫事務內。
這是在流管理持久化中使用只讀事務優於使用非事務性數據訪問的地方。
如上所示,流管理持久化解決了涉及原子 Web 流的用例。本文剩餘部分將重點介紹調用非原子 Web 流的用例,其中未應用流管理持久化。注意在其中的某些用例中我們仍然能夠使用流作用域的持久化上下文對象。
非原子 Web 流
從業務流程管理 (BPM) 角度而言,一種長期運行的進程比典型的 Web 會話存活時間長。如果此類長期運行的進程涉及了人類的任務, 則用戶可以在該進程中工作任意長的時間,並且可以在幾小時、幾天甚至幾個月之後回來恢復進程的執行。顯然,此類進程應該在服務器崩潰時也可以存活。
所有這些因素都表明每一次進程運行後長期進程的狀態都需要持久保存到後端數據庫。將此長期運行進程的人類活動實現爲 Web 流成爲一個明智的技術解決方案。流將在不同的 Web 會話中重複執行以模仿長期進程的生命週期。
除了上述場景,還有一些應用程序由非上下文 Web 頁組成,用戶可以在這些 Web 頁之間任意導航。這些 Web 頁可以根據其業務功能被分組成流,即使沒有邏輯順序流也沒有開始或結束狀態。每個用戶請求期間所做的數據變更都要被保存。這些應用程序中的持久化編程與上述長期運行的進程沒有什麼不同,事務原子性的作用域都劃定到每個用戶操作而不是一系列用戶操作 —一個 Web 流。
非原子 Web 流用例
在醫療衛生行業,服務提供商定期接觸患有慢性病的成員以評估他們的健康狀況和潛在風險。健康提供商隨後爲其提供治療和行爲健康方面的建議。這稱爲 案例管理。
案例管理系統圍繞着一系列聯繫任務。在一個典型任務中,案例經理會通過電話聯繫一個成員,詢問評估問題並根據其回答給出適當的建議、創建轉診請求、記錄聯繫結果和設置後續任務。
情況非常複雜。評估問題清單可能很長:電話可能由於各種原因而被中斷、沒有記錄轉診某些任務可能無法完成等等。包含併發或異步操作的聯繫任務是一個長期運行的進程,每一步進展都要被保存到數據庫。聯繫任務可以被模擬爲單一的 Web 流,在長期運行進程的發展過程中,它可以被重複進入和執行。
Spring Web Flow 文檔沒有介紹此非原子 Web 流場景。在此用例中仍然可以利用流作用域的持久化上下文對象嗎?答案是可以。
指定事務的作用域
我們知道流定義文件中的 <persistence-context/>
標籤爲我們提供了一個綁定到線程的 flowScoped
持久化上下文,這帶來了 沒有分離實體和 沒有LazyInitializationException
異常的好處。因此,我們選擇保留這個標籤。與原子流中流管理的持久化相比,事務作用域發生的最大變化時:原子性應用於進程的每一步而不是整個流。通常,進程中的原子步驟是由
Web 流定義中的 <transition>
標籤表示的一個用戶操作。
令人失望的是 Spring Web Flow 在其任何標籤上都不支持事務分界,包括 <transition>
和 <evaluate>
。開發人員的下一個選擇是從註釋有@Transactional
的
Spring bean 方法發起數據庫事務並從 <evaluate>
標籤調用該方法(<transition>
標籤不支持方法調用)。
從本質上來說,事務的作用域劃定到流中的 <evaluate>
標籤。應用 @Transactional(readyOnly=false)
將使
JPA/Hibernate FlushMode
設置爲 AUTO
,這樣 Hibernate 會確定在同一事務的上下文內何時刷新數據變更。爲了簡化編程和優化 SQL,在這些用例中使用自動刷新要優於手動刷新。注意在同一 <transition>
下允許多個 <evaluate>
標籤,會導致每用戶操作出現多個數據庫事務。
如果每個用戶操作 / 請求被認爲是原子性的,通常事實也是如此,我們希望將所有數據庫寫操作分組到一個 Spring bean 的一個@Transactional
方法內,這樣它們就綁定了同樣的事務上下文並通過同樣的 <evaluate>
標籤調用。清單
1 展示了我們如何指定原子請求的事務上下文。
清單 1. 指定原子用戶操作的事務上下文
<transition> <evaluate expression="beanA.readAlpha()"/> <evaluate expression="beanA.readBeta()"/> <evaluate expression="beanB.readGamma()"/> <evaluate expression="beanA.writeAll()"/> <!-- a single read/write transaction --> <evaluate expression="beanB.readEta()"/> </transition>
清單 2 展示了一個非典型的案例,其中同一個用戶請求中涉及了多個讀 / 寫事務(獨自提交或回滾)。因此,用戶請求變成了非原子的,這在大部分開發場景中是災難性的。
清單 2. 爲非原子用戶操作指定事務上下文
<transition> <evaluate expression="beanA.readAlpha()"/> <evaluate expression="beanA.readBeta()"/> <evaluate expression="beanB.readGamma()"/> <evaluate expression="beanA.writeDelta()"/> <!-- read/write transaction --> <evaluate expression="beanA.writeEpsilon()"/> <!-- read/write transaction --> <evaluate expression="beanB.writeZeta()"/> <!-- read/write transaction --> <evaluate expression="beanB.readEta()"/> </transition>
我們如何處理那些同一 <transition>
下的其他 <evaluate>
標籤引用的只讀操作?我們有三個選擇:
- 如前所述,不帶有任何數據庫事務地運行只讀操作。
-
將其標記爲
@Transactional(readOnly=false)
,這樣可以在讀 / 寫數據庫事務下執行 SQL 查詢。在這種情況下,流作用域持久化上下文的FlushMode
將一直是AUTO
。 -
用
@Transactional(readOnly=true)
標記它們。這樣,對於那些只讀事務FlushMode
變成MANUAL
而在遇到讀 / 寫事務時又會過渡到AUTO
。
在持久化上下文中,JPA/Hibernate 會在提交讀 / 寫事務前自動刷新未決變更。爲了簡單,Hibernate 團隊鼓勵開發人員在此類情況下,在所有數據操作中都一致地應用讀 / 寫事務。只需向所有應用 @Transactional
的地方設置 readOnly=false
即可。
意料之外的 OptimisticLockingFailureException 異常
在非原子 Web 流中使用流作用域的持久化上下文,您可能會遇到一些意料之外的OptimisticLockingFailureException
異常。
對於非原子 Web 流還是強烈推薦使用樂觀鎖來保護每個用戶操作的數據完整性。實體的@Version
字段是一個數據庫生成的整數或時間戳且隨後伴有更新操作時,需要顯式地查詢實體以便在持久化上下文中刷新其狀態。否則,@Version
字段將帶有陳舊的值而後續在不同事務中對同一實體的更新會導致 OptimisticLockingFailureException
異常。具有諷刺意味的是,這個異常將在沒有多用戶併發性的情況下發生。相反,在原子流中必須避免這種更新後查詢,否則將發生提前刷新。畢竟,無論在原子
Web 流期間,在內存中對實體對象更新了多少次,流結尾處發生的 SQL 刷新只能看到實體實例的最後狀態。
顯然流作用域持久化上下文使原子和非原子 Web 流中的持久化編程更順利、更簡單。不使用流作用域持久化上下文對象的 Web 流中的持久化編程也是可行的,只是有很多障礙和缺陷。
不使用流作用域持久化上下文的持久化編程
在某些情況下,如 Hotel Booking 樣例應用程序所示,可以在沒有 <persistence-context/>
標籤的情況下實現
Web 流。這種方法最明顯的影響在原子 Web 流上,一旦省略了流作用域持久化上下文對象您就無法得到原子 Web 流了。我將在下文討論其他不便之處。
持久化上下文作用域劃定到數據庫事務
不使用流作用域持久化上下文,通過 @PersistenceContext
註釋注入的持久化上下文默認情況下作用域劃定到數據庫事務。要更好地理解這種操作的問題所在,請查看來自
Hotel Booking 樣例應用程序的以下代碼片段:
清單 3. Hotel Booking 中 “主流” 定義的代碼片段
<view-state id="enterSearchCriteria"> <on-render> <evaluate expression="bookingService.findBookings(currentUser.name)" result="viewScope.bookings" result-type="dataModel" /> </on-render> <transition on="cancelBooking"> <evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" /> <render fragments="bookingsFragment" /> </transition> </view-state>
如果清單 3 中引用的 cancelBooking
方法定義如下
:
清單 4. cancelBooking 方法
@Service("bookingService") @Repository public class JpaBookingService implements BookingService { //... @Transactional public cancelBooking(Booking booking){ if (booking != null) { em.remove(booking); } }
那麼運行此代碼時我們將得到如下錯誤:
Caused by: java.lang.IllegalArgumentException: Removing a detached instance
<on-render>
標籤返回的 booking
實體會在後續操作 <transition
on="cancelBooking">
中分離。同一 bookingService
bean
的兩個方法findBookings
和 cancelBooking
在不同的數據庫事務下執行,因此與兩個截然不同的持久化上下文對象相關聯。一個持久化上下文管理的booking
實體,從另一個持久化上下文的角度來看,是分離的。
爲了規避這一問題,在清單 5 所示的實際 cancelBooking
方法中,同一 booking
實體在它被刪除前通過其主鍵被重新加載。
清單 5. 修復的 cancelBooking 方法
@Service("bookingService") @Repository public class JpaBookingService implements BookingService { //... @Transactional public cancelBooking(Booking booking){ booking = em.find(Booking.class, booking.getId()); // reinstate the persistent entity if (booking != null) { em.remove(booking); } }
以事務爲作用域的持久化上下文與帶有 singleSession=false
的 OpenSessionInViewFilter
/Interceptor
的工作方式一樣。
這意味着同一請求中的每個事務都有其自己的相關會話。但是這裏我們失去了 Open Session in View 的 “延遲關閉模式” 的優勢。
在視圖呈現期間,延遲讀取將導致 LazyInitializationException
異常,因爲以事務爲作用域的持久化上下文會在每個事務完成後立即關閉。也可以選擇實現類似於 OpenSessionInViewInterceptor
/ OpenEntityManagerInViewInterceptor
之類的東西,但是
Spring 核心包提供的那些東西不能開箱即用地爲 Spring Web 工作。使用內置的流作用域的持久化上下文對象要簡便得多!
作用域爲每個調用的持久化上下文
沒有流作用域持久化上下文協助的非事務性數據訪問是情況最糟糕的場景,應該儘量避免。
事務之外,持久化上下文的作用域被劃定到帶有 FlushMode
AUTO
和 auto-commit
true
設置的每個調用(記住 Hibernate 爲非事務性數據訪問禁用了自動刷新)。換句話說,對通過 @PersistenceContext
注入的同一持久化上下文代理的每個方法調用都將返回一個不同的實體管理器實例,且這些實例會立即打開而後關閉。
從本質上來說,實體管理器的作用域劃定成了 “短事務”。您在 清單 5中看到的同樣的代碼片段將會發出以下的錯誤消息:
java.lang.IllegalArgumentException: Removing a detached instance
在不同的流中傳遞實體
這是最後一個有時會引起問題的場景:需要在不同流中傳遞實體時會出現什麼情況?
流作用域的持久化上下文對象服從於流的作用域,因此一旦從一個流向另一個流傳遞實體,這些實體就會立即分離。解決方法是將這些實體合併 / 重新連接到當前流的持久化上下文或者用其主鍵重新加載它們,這種策略仿效了 Open Session in View 方法。
結束語
Spring Web Flow 是一種先進的 Web 開發框架,提供了獨特的功能以支持使用 JPA/Hibernate 的持久化編程和事務管理。本文探討了 Java 開發人員在編寫 Spring Web Flow 應用程序中面臨的困難和挑戰。
從真實世界用例中(比如本文介紹的用例),對於在 Spring Web Flow 中編碼事務性原子和非原子 Web 應用程序,我總結出了以下 “規則”:
- 首選的是使用流作用域持久化上下文
- 全局性地將只讀事務應用到原子 Web 流中引用的所有方法
- 全局性地將讀 / 寫事務應用到非原子 Web 流中引用的所有方法