1. 程式人生 > >JPA與CMT – 為什麽單純捕捉持久化異常是不夠的?

JPA與CMT – 為什麽單純捕捉持久化異常是不夠的?

在那 not 解決 nes ets mar nsa detach 問題

在EJB和JPA架構中使用CMT(容器控制事務)是很簡單的。只要定義一些註解來標示事務邊界(或使用默認),無需再手動使用開始、提交或回滾操作。在EJB的業務方法中唯一回滾事務的方法就是拋出非應用異常(或者使用rollback=true標註應用異常)。看起來很簡單:如果在某些操作中有可能會拋出一個異常,但你不希望事務回滾只需要捕獲異常就可以了。你能夠在同一個仍然活動的事務中再次重試這個不穩定的操作。

對於用戶組件拋出的應用異常來說是完全成立的。問題是從其他組件拋出的異常會怎麽樣?比如JPA的EntityManager拋出的一個PersistenceException?這就是撰寫本文的原因。

我們想要實現什麽

想象一下如下場景:你有一個名為E的實體。包括如下屬性:

id–主鍵,
name– 可讀的實體名稱,
content– 一些存放字符串的隨意字段 – 用於模擬“高級屬性”例如在持久化/更新時計算的字段並且可能導致錯誤的。
code– 保存OK 或者ERROR字符串 – 定義高級屬性是否成功保存。
希望持久化E。假設E的基礎屬性一直都能成功持久化。而高級屬性,需要一些附加計算或操作可能會導致例如數據庫拋出的違反約束。當這樣的情況發生後,你仍然希望E能夠持久化到數據庫中(只是基礎屬性填充,code字段設置為ERROR)
換句話說你可能希望:

持久化E的基本屬性
嘗試更新高級屬性
若步驟2中拋出了PersistenceException – 捕獲該異常並設置‘code’屬性為ERROR並且清除所有高級屬性(因為會引發異常)

更新E
普通解決方案
在EJB代碼中展示你可能會如何執行(假設為默認的事務屬性(TransactionAttributes))

public void mergeEntity() {
MyEntity entity =newMyEntity(‘entityName‘,‘OK‘,‘DEFAULT‘);

em.persist(entity);

// This will raise DB constraint violation
entity.setContent(‘tooLongContentValue‘);

// We don‘t need em.merge(entity) - our entity is in managed mode.

try{
em.flush(); // Force the flushing to occur now, not during method commit.
}catch(PersistenceException e) {
// Clear the properties to be able to persist the entity.
entity.setContent(‘‘);
entity.setCode(‘ERROR‘);

// We don‘t need em.merge(entity) - our entity is in managed mode.
}
}
1

示例中有什麽問題?
捕獲EntityManager 拋出的PersistenceException 不能夠阻止事務回滾。這不像不捕捉EJB中的異常會使得事務回滾,而是EntityManager 拋出的非應用程序異常會將事務標記為回滾。更不用說資源內部可能會標記事務為回滾。意味著實際上你的程序並不能真正控制事務的行為。同時,作為事務回滾的結果,我們的實體被轉移至遊離(detached )狀態。因此,在方法的最後 em.merge(entity)這樣的方法還是需要的。

有效方案
該如何應對事務自動回滾呢?因為我們使用CMT,所以我們唯一的方法就是定義另一個業務方法開始一個全新的事務,並且將所有可能導致異常的操作在那裏執行。這樣的話即使PersistenceException 被拋出(並且捕獲)它只會標註當前新事務回滾,我們的主事務是不會有影響的。如下可以看到一些樣例代碼(為了簡潔去除了日誌代碼):

public void mergeEntity() {

MyEntity entity =newMyEntity(‘entityName‘,‘OK‘,‘DEFAULT‘);

em.persist(entity);

try{
self.tryMergingEntity(entity);
}catch(UpdateException ex) {
entity.setContent(‘‘);
entity.setCode(‘ERROR‘);
}
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void tryMergingEntity(finalMyEntity entity) throws UpdateException {
entity.setContent(‘tooLongContentValue‘);

em.merge(entity);

try{
em.flush();
}catch(PersistenceException e) {
thrownewUpdateException();
}
}
註意:
更新異常(UpdateExceptionis )也是繼承自Exception類的應用異常(所以默認是rollback=false)。用於表示更新動作失敗。作為一個可選方式,你可以將 tryMergingEntity(-)方法的返回值由void轉為boolean。用這個boolean結果來表示更新動作成功與否。

Self在我們的EJB中是對自身的引用。這在EJB容器代理中是一個必須步驟,使得被調用的方法能夠使用@TransactionAttribute,作為一個可選方式,可以使用SessionContext#getBusinessObject(clazz).tryMergingEntity(entity).

em.merge(entity) 方法是很關鍵的。我們在tryMergingEntity(-)方法中開啟一個新事務,所以實體不會存在於持久化上下文中。
方法中無需再有其他的更新或刷新操作了。CMT的常規特性確保了事務不會被回滾,這意味著實體的所有改動都會在事務提交時自動刷新。
我們再次強調一下底線:捕獲異常並不意味著你當前的事務不會被標記為回滾。PersistenceException 並發應用程序異常,但無論是你否捕捉都會導致你的事務回滾。
JTA BMT解決方案
一直以來我們都討論的是CMT。那JTA BMT呢?找到下面代碼中展示如何使用BMT處理這個問題作為獎勵吧:

public void mergeEntity() throws Exception {
utx.begin();
MyEntity entity =newMyEntity(‘entityName‘,‘OK‘,‘DEFAULT‘);
em.persist(entity);
utx.commit();

utx.begin();
entity.setContent(‘tooLongContentValue‘);

em.merge(entity);

try{
em.flush();
}catch(PersistenceException e) {
utx.rollback();

utx.begin();
entity.setContent(‘‘);
entity.setCode(‘ERROR‘);

em.merge(entity);
utx.commit();
}
}
使用JTA BMT我們能夠在一個方法中處理所有的問題。是因為我們能夠控制事務何時開始與提交/回滾(查看上述代碼中的utx.begin()/commit()/rollback())。然而結果還是一樣的,在拋出PersistenceException 之後,我們的事務還是被標記為回滾,你可以使用UserTransaction#getStatus() 來查看並且與常量Status.STATUS_MARKED_ROLLBACK進行比較。

JPA與CMT – 為什麽單純捕捉持久化異常是不夠的?