虽然我们实现了这个银行转账的功能 ,但是在这个项目中,我们其实还存在一个很严重的问题 , 那就是事务回滚, 我们来看service层的这段代码:
public String transform(String fromActno , String toActno , double money) {
// 在进行逻辑判断之前 , 首先要获取到转出账户和转入账户的信息
// 需要通过传来的账户同伙dao层进行查找数据
Bank fromAct = dao.findAccountByActno(fromActno);//转账账户
Bank toAct = dao.findAccountByActno(toActno); //收款账户
// 首先判断转账账户的金额是否充足
if(fromAct.getBalance()<money){
// 余额不足
return "余额不足" ;
}
// 更新账户信息
// 先更新数据
fromAct.setBalance(fromAct.getBalance()-money);
// 然后执行更新sql语句
dao.updataAccountByActno(fromAct);
// 这里没有进行事务回滚,所以这里报错之后,会导致上面的数据已经更新,而下面收款的数据却没有更新,从而损失
// 所以我们的思路就是:让它的sqlsession是同一个对象,只有两个数据都完成更新后才提交事务。
// 那么如何让sqlSession对象是同一个呢?我们在创建sqlsession的对象的时候用ThreadLocal获取存储的对象
String str = null ;
str.toString();
toAct.setBalance(toAct.getBalance()+money);
dao.updataAccountByActno(toAct);
// 当整个转账逻辑执行完之后 , 我们在统一提交和关闭
sqlSession.commit();
SqlSessionUtil.close(sqlSession);//这里使用工具类的close方法关闭sqlSession对象
// 返回更新成功的信息
return "更新成功" ;
}
从上图中我们可以看见,如果在第一次更新转账账户数据后,发生了异常,而第二次接收账户用于发生了异常导致没有更新数据 , 那么就造成了钱已经扣掉了,但是收款账户却没有收到钱,从而白白损失掉了这部分钱。
所以,我们应该如何解决这个问题呢?
首先 , 我们先来看看这两次调用dao层更新数据中的dao层的代码:
public void updataAccountByActno(Bank bank) {
SqlSession sqlSession = SqlSessionUtil.openSession();
sqlSession.update("updataActno" , bank) ;
sqlSession.commit();
}
其实,我们会发现 , 之所以发生这种情况,就是因为在这两次更新数据调用dao层的过程中,dao层都会重新获取一个SqlSession对象 , 然后更新数据并提交,导致service层第一次更新数据就已经提交了。
所以 , 我们只需要在service创建一个SqlSession对象 , 然后让这个SqlSession对象去执行整个更新数据的方法,比如当他执行往第一次数据更新后,并不直接提交数据 , 而是继续执行第二次更新数据的操作 , 如果两个数据更新的操作都执行完成之后 , 再去提交数据 , 这样就算中间发生了异常 , 因为第一次更新数据后SqlSession对象并没有提交数据 , 所以也不会造成损失。
那么,我们现在思路就很清晰 , 首先我们需要一个SqlSession对象去执行service层的更新方法 , ,所以我们就不能在dao层去创建SqlSession对象,同时也不能再dao层提交数据 , 而要在service层当整个方法都执行完之后再去提交。
所以 ,这里就有一个问题 , 我们因为我们在dao层需要创建一个SqlSession对象去执行sql语句 , 但是每次调用SqlSession.opensession()都会创建一个新的对象 , 我们如何保证在这两次更新数据中用的都是同一个sqlSession对象呢?
其实,一个有两种方法 , 第一种就是在service创建一个SqlSession对象 , 然后在dao层不再创建新的SqlSession对象 , 而是把service层的SqlSession对象通过参数传过去 ,代码如下:
dao层就不需要再创建Sqlsession对象, 只需要接收service传来的对象来执行sql语句 , 这样就能保证整个过程都是同一个SqlSession对象。
dao层代码如下:
public void updataAccountByActno(Bank bank , SqlSession sqlSession) {
// SqlSession sqlSession = SqlSessionUtil.openSession();
sqlSession.update("updataActno" , bank) ;
// 因为涉及到事务 , 所以我们不能在dao进行提交
// sqlSession.commit();
}
虽然这种方法可以解决这个问题 , 但是dao层的接口都是写好的,大部分都不会让你私自去改参数的 , 你总不能自己去给接口加个参数 , 那么后面所有于此相关的代码都要改 , 显然实际开发中并不会用这种方式去做事务回滚。
所以这就需要我们使用另外一种方法:使用ThreadLocal。
其实我们的思路还是没有变 , 就是要使用一个SqlSession对象去执行这个数据更新。只不过换了一种方式。
现在我们需要使用ThreadLocal去存储一个sqlSession对象,我们在service层创建好一个sqlSession对象 , 然后把它存到ThreadLocal中去,然后在dao层中我们还是继续创建一个sqlSession对象 , 但是创建方法我们需要改,并不是直接调用SqlSession.opensession()方法去创建 , 因为这样一定是一个新的sqlSession对象 , 我们从ThreadLocal中获取 , 因为刚刚在service层我们已经创建了一个sqlSession对象并且存进去了,所以我们就是直接取这个sqlSession对象 , 这样就导致我们dao层每次获得的sqlSession对象都是同一个。
工具类中的获取SqlSession对象方法代码如下:
然后service层代码如下:
最后,在关闭的时候也需要重新写一下 , 代码以及注释如下:
public static void close(SqlSession sqlSession){
// 为什么不直接使用sqlSession中的close()方法去关闭对象 , 还要在工具类里面专门写一个close方法呢?
// 因为我们选择的sqlSession对象是存储在ThreadLocal里面了, 所以我们关闭了SqlSession对象后 ,
// 我们还需要在ThreadLocal中清除它 , 否则会造成sqlSession复用 , 但是因为它关闭了,从而用了一个没用的
sqlSession.close();//关闭
local.remove();//同时移除
}
整个事务回滚到此就结束了,希望通过这个案例对事务有一个更深层的理解 , 加油!