2013年3月9日 星期六

Spring 宣告式交易管理

聽起來很華麗,用起來也很過癮,但是一開始沒有搞的很清楚,加上又找不到詳細的解釋,在Stackoverflow上問了問題,再參照其它的文章,才搞懂他真正的用法。


本文章的關鍵字是 : Transaction propagation , declarative transaction,Spring宣告式交易管理。



首先先講測試的架構,都是用annotation來做。前面有介紹過方法了。

1。 發起方是一個action裡的method,它會call UserService一個 testAddUserButAddCountryFail的方法。

2. 在這個service裡面,注了一個dao,與另一個service。

3. 這個testAddUserButAddCountryFail方法內我們做二件事: 首先呼叫dao 來insert 一筆資料,這邊我們會讓它insert 成功。再來用另一個service來呼叫,這個service裡面我們會對交易的方式做測試,都是要讓他fail ,但是要看他對testAddUserButAddCountryFail 產生什麼影響。

看程式比較容易:

UserService裡


     @Resource
   private UsersDAO userDao;
     @Resource
    CountryService countryService;


     @Transactional(value="deu"   )
    public void testAddUserButAddCountryFail(){
      userDao.addUser();
      countryService.addCountry();   
  }

CountryService 裡


     @Resource
   private CountryDAO countryDao;

@Transactional(value="deu"  )
public void addCountry(){  
countryDao.addCountryFail();
}

測試 一 : CountryService中的@Transactional 不做什麼設定,預設就會是串在一起的交易。所以當 countryService.addCountry(); 裡面報錯時,exception丟出來 後,整個交易都會失敗,所以user不會新增成功。

如果我們把  countryService.addCountry(); 包try catch不讓它擴散呢?  不行噢,當程式呼叫 countryService.addCountry(); 時,addCountry已被視為是同一個交易,當countryService.addCountry(); 裡出問題自動rollback 時,已經馬上影響到addUser了,也就是原來add的user在一出問題時已經被rollback了。當程式回到 testAddUserButAddCountryFail 你想再處理時已經來不及了。

測試二 : 將


@Transactional(value="deu"  )
public void addCountry(){  
countryDao.addCountryFail();
}



改為


@Transactional(value="deu", propagation=Propagation.REQUIRES_NEW  )
public void addCountry(){  
countryDao.addCountryFail();
}


我們想要知道,根據Spring的文件來說,這邊會變成一個新的交易,我們要讓它出錯,看影響的狀況如何。我們預期它是一個新的交易,所以它出錯不會影響到新增的User.

結果呢…新增的user還是被rollback了。這一點讓我百思不解。後來研究了網站上的說明,才發現 addCountry雖然是一個新的交易,它自己也rollback了,但是它的exception還是會丟回外部的交易(也就是原來addUser開的那一個),因為我們在

@Transactional(value="deu"  )

public void testAddUserButAddCountryFail(){
   userDao.addUser();
   countryService.addCountry();    
 }

中沒有做任何處理,這個exception間接導致了 testAddUserButAddCountryFail 也rollback了。

解決方式 : 在    countryService.addCountry(); 加try catch,白話來說就是 : 它是一個獨立的交易,要是裡面出錯了,它的exception不對我( testAddUserButAddCountryFail ) 產生任何影響。

這邊懂了之後,順便講一下Spring的術語:

當程式一進去 testAddUserButAddCountryFail 時,因為我們有@Transactional ,所以這邊是一個新的交易,叫outer transaction,也就是外面的交易。當我們  countryService.addCountry(); 的時候,因為  addCountry() 裡也是一個交易,這邊叫 inner transaction (內部) 。整個測試案例就是在測試,inner一但有exception時如何對 outer 產生影響。

propagation=Propagation.REQUIRES_NEW會把inner transaction變成一個獨立交易,不寫的話會變成inner的一部份。雖然透過 propagation=Propagation.REQUIRES_NEW變成了一個獨立交易,但是outer還是要對它丟出的例外做處理,都不處理的話,這個例外會影響到原有的交易行為,變成跟著也rollback了。

我這邊順便再貼一下JB Nizet 的回應,他講的夠清楚了。

You got it right. 

If the second service uses REQUIRED, both services are in the same transaction, and the transaction will be marked for rollback if an exception is thrown by the second (or first) service. Catching the exception in the first service won't change anything: the transaction is marked for rollback already. 

If the second service uses REQUIRES_NEW and throws an exception, it has its own transaction, which will be rollbacked. But the exception, as any other exception, will propagate to the caller (the first service). So if it doesn't catch it, its transaction will also be rollbacked. – JB Nizet









沒有留言:

張貼留言