眾所周知,資料庫有交易處理(Database Transaction),當一個事務中的操作沒有能全部進行時,之前的操作將復原。
如果操作都在同一個資料庫上,那可以直接使用資料庫事務進行處理,但是如果跨資料庫操作呢?可以使用JTA。來看看百度百科中JTA的解釋:“JTA,即Java Transaction API,譯為Java事務API。JTA允許應用程式執行分散式交易處理——在兩個或多個網路電腦資源上訪問並且更新資料。”。有興趣的朋友可以搜一下JTA的用法。
把復原放在業務層有利有弊
利在於可以不用增加DAO層的代碼,DAO層只單純扮演資料讀寫的角色,操作的粒度很細。細粒度意味著DAO層介面可以擁有更好的複用性。並且,如果不使用JTA,那麼業務層中將不會混入與SQL相關的語句。所有與DB有關的部分都被封裝在DAO層不會泄露到上層。當DB被更換時,只需要更改DAO層的資料介面代碼,而不需要改動業務層代碼。
弊在於在業務層用代碼實現復原是一件複雜的事情,需要做一步一步的判斷,並且復原指令是累加的。
為什麼說復原指令是累加的呢?
假設現在有4個操作(operation1~4),只有當這4個操作都順利進行時才接受,否則就復原之前進行的操作。
那麼大致的邏輯就是(虛擬碼,假設有四個替換操作,每個替換都成功時才算正確,否則將復原之前的操作):
public boolean rollBackExample(String oldID, Object newOne){ boolean res = true; Object oldOne = getObject(oldID); res = Replace1(oldID, newOne); if(res){ //operation1 success //now doing operation 2 res = Replace2(oldID, newOne); if(res){ //operation2 success //now doing operation 3 res = Replace3(oldID, newOne); if(res){ //operation3 success //now doing operation 4 res = Replace4(oldID, newOne); if(res){ return true; }else{ //rollback Replace3 \Replace2 \ Replace1 Replace3(newOne.getID(), oldOne); Replace2(newOne.getID(), oldOne); Replace1(newOne.getID(), oldOne); return false; } }else{ //rollback Replace2 \ Replace1 Replace2(newOne.getID(), oldOne); Replace1(newOne.getID(), oldOne); return false; } }else{ //rollback Replace1 Replace1(newOne.getID(), oldOne); return false; } }else{ return false; } }
可以看到,代碼中進行逐級進行了判斷,並且依據操作進行程度的加深,復原的列表逐漸增多。把復原的操作單獨提出來可以看得更明顯些:
當第二個操作出錯時,只需復原
//rollback Replace1 Replace1(newOne.getID(), oldOne);
當第三個操作出錯時,需要復原:
//rollback Replace2 \ Replace1 Replace2(newOne.getID(), oldOne); Replace1(newOne.getID(), oldOne);
當第四個操作出錯時,需要復原:
//rollback Replace3 \Replace2 \ Replace1 Replace3(newOne.getID(), oldOne); Replace2(newOne.getID(), oldOne); Replace1(newOne.getID(), oldOne);
假設這個事務有N個操作組成,那麼當進行到第N個操作時出錯,需要進行N-1項復原。而累積的代碼為1 + 2 + …… + N - 1 = N(N-1)/2行代碼,直觀點看就是如果有10項操作,那麼理論上將有9項可能的復原操作,並且在函數中將累計出現45行用於復原的代碼。用於描述復原的代碼的平均重複出現次數達5次。非常拖遝。
要如何解決這個代碼不優雅的問題呢?
首先,判斷條件是不可少的,也就是if-else語句無法省略。因為operationj可能是在operationi(j later then i)的基礎上啟動並執行,因此需要一步步判斷以避免出錯。
其次,不管是哪一步出錯,它進行復原的操作都是與自己所處的執行深度成正相關的。當第k步出錯時,k-1及之前的步驟就需要復原,每一個操作都是如此。這個性質可以在沒有寫break的switch語句中找到影子。當case1執行後,會接著執行case2……以此類推。
因此我們可以將需要進行的復原操作設計到一個switch-case語句中,虛擬碼如下:
public boolean rollBackExample2(String oldID, Object newOne) { boolean res = true; Object oldOne = getObject(oldID); int phase = 0; res = Replace1(oldID, newOne); if (res) { res = Replace2(oldID, newOne); if (res) { res = Replace3(oldID, newOne); if (res) { res = Replace4(oldID, newOne); if (res) { phase = 4; } } else { phase = 3; } } else { phase = 2; } } else { phase = 1; } switch (phase) { case 4: return true; case 3: Replace3(newOne.getID(), oldOne); case 2: Replace2(newOne.getID(), oldOne); case 1: Replace1(newOne.getID(), oldOne); default: return false; } }
可以看到,當使用switch-case結構+phase階段判斷時,就不會出現復原指令的代碼冗餘了。