众所周知,数据库有事务处理(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阶段判断时,就不会出现回滚指令的代码冗余了。