Domain Service Owner Transaction Design Pattern
Domain Service Owner Transaction Design Pattern 這一個詞是由這本書: Java Transaction Design Strategies 所命名的。這個 pattern 我們 team 已經用了一段時間,我歸納了一些有關 exception 處理的須知和小技巧,大家可以參考參考。
目前我們 team 的 spring transaction 設計是採用 txProxyTemplate:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager" />
</property>
<property name="transactionAttributes">
<props>
<prop key="save*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
<prop key="update*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
<prop key="delete*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
<prop key="remove*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
<prop key="tx*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
<prop key="*">PROPAGATION_REQUIRED,readOnly, -org.bioinfo.util.BusinessException</prop>
</props>
</property>
</bean>
上面寫著只要是 save*,update*,delete*,remove*, 和 tx* 等開頭的 method 時,就會開啟 read-write transaction,其他的 method 則是開啟 read-only transacton。按這個 convention,我們的 XXXService 就不用一個一個 method 去調整 transaction 的設定。
上面的設定裡,都加了一行 -org.bioinfo.util.BusinessException。這個表示當你的 XXXService 的 method 丟出 BusinessException (或它的 subclass) 時,transaction 會作 rollback。以下面的例子為例:
public class ProjectException extends BusinessException {
//.... skip
}
public class ProjectServiceImpl implements ProjectService {
public void txCreateProject() throws ProjectException {
//.... do something...
}
}
按我們的設定,當 projectService.txCreateProject() 丟出 ProjectExeception 時,則 spring 會將 transaction rollback。除此之外, 當 txCreateProject() 丟出 RuntimeException 也會 rollback。這是 spring 內定的 rule,所以我們不用特別去寫。
上面這一套 convention 是我們 team 現在採用的作法,簡單易寫而且不易出錯。不過這個作法目前有幾個限制要額外注意。
除了 BusinessException 之外,其他 CheckedException 不會作 rollback
//錯誤示範!
public class ProjectServiceImpl implements ProjectService {
public void txAttachFiles() throws IOException {
//.... do something...
}
}
上面的例子裡,設計這個 txAttachFiles() method 的人,將 IOException 丟出來,讓外面來處理。這時問題就大了,IOException 不是 BusinessException 的 class,如果寫檔案失敗,那 transaction 並不會做 rollback!這個 method 有可能會將不全的資料寫入資料庫並且 commit。
要改進這個問題,我們該寫成這樣:
public class ProjectServiceImpl implements ProjectService {
public void txAttachFiles() throws ProjectException {
try {
//.... do something...
} catch (IOException e) {
//轉為 BusinessException 或是 RuntimeException
throw new ProjectException(e);
}
}
}
上面我們將 IOException 轉為 ProjectException 出去,這樣就會 rollback 了。如果這個 exception 用戶沒辦法處理時,我們也可以轉為 RuntimeException。
那麼,是否有一個特殊的狀況是--"我希望丟出 exception,但不要 rollback" 呢 ? 我相信這個狀況是有的,但是應該很少。因為即然丟出了 exception,不就是代表 裡面出了問題而無法進行下去 嗎?出了 問題 卻還想 commit,這樣的思維是矛盾的。如果你在設計上出現了 "丟出 exception 不想 rollback",那很有可能你 錯把 exception 當做 flow control (即當做 if) 來用了。exception 就是例外狀況,是不可預期的,而不是程式流程中一個已知的分支。
當 service 互相呼叫時,其中一個丟出 RuntimeException 或是 BusinessException,則整個 request 會強制 rollback,無法回復。
public class ProjectServiceImpl implements ProjectService {
private UserService userService ;
public void txCreateMembers() throws ProjectException {
try {
//.....
User newMember = userService.txCreate() ;
//.....
} catch (UserCreateException e) { //這個會讓整個 transaction 變成 rollback
//catch 後這裡我們可以做..... ?
}
projectDAO.save(project);
}
}
上面這個例子 ProjectService 引用 userService 來建立 member,但建立 member 時有可能會丟 UserCreateException (是一個 BusinessException)。依我們的 txProxyTemplate 的設定,userService.txCreate() 丟出 BusinessException 後,整個 transaction 就 rollback 了。換句話說,即使 catch 了 UserCreateException,想做一些回復的動作是徒勞無功的,資料還是不會正常的 commit() (後面那一行 projectDAO.save(project) 無效了)。這是我們現在 txProxyTemplate 設計上的限制,請大家注意使用。
在這個限制下,我們無法回復裡面 service 丟出 BuinessException 所造成的 rollback。因此當我們 catch 到 exception 時,處理的方式只剩下再丟出 exception:
public void txCreateMembers() throws ProjectException {
try {
//.....
User newMember = userService.txCreate() ;
} catch (UserCreateException e) { //強制 rollback
/* 這裡我們不用費神做回復了,因為回復不了...
能做的只剩下再丟出 exception */
//做法(1) 丟出原來的,也就是乾脆不 catch 了。
throw e ;
//做法(2) 轉為這個 Service 用的 BusinessException
throw new ProjectException(e) ;
//做法(3) 轉為 RuntimeException
throw new RuntimeException(e) ;
}
}
做法(1) 是直接丟出原來的 UserCreateException,這個做法最簡單,但是這會污染 ProjectService 的 API,因為 ProjectService 的 API 開始和 User的 API 相關聯了,失去封裝的效果。做法 (2), (3) 是比較建議的作法,選哪一種就看上一層該不該處理 UserCreateException 了。
雖然我們現在 txProxyTemplate 有這個限制,但這個限制還算有道理的 -- 因為當 method 選擇丟出 exception 時,表示內部的狀態已經損毀,本來就不期望你能回復什麼,而是希望你能全部重來。因此大部份的應用應該沒什麼問題。如果真的遇到少數的特例,那就不要用 txProxyTemplate 囉。
總結 txProxyTemplate 的注意事項
- 不要在 service method 上 throw 非 BusinessException 的 CheckedException。除非你有充份的理由。
- 當 service 內部呼叫其他 service method 時,如果內部的 service throw 了 BusinessException,你能做的只剩再丟出(或轉丟出) exception,無法做回復。