一.工具
在介绍事务的propagation属性前,我们首先介绍一个工具:Grep Console,该工具用来实现将idea输出出的日志信息进行选择性的高亮展示。
当要选择日志中的某一部分高亮展示时,只需要右键点击Add Highlight即可。此时日志中所有的该部分都会变为高亮。
二.propagation属性
propagation属性是用来控制事务的传播行为的。所谓事务的传播行为,就是指当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。例如a()方法是一个事务,在a()方法中又调用了b()方法,b()方法又是一个事务,那么这两个事务是共用一个事务,还是b()再重新开启一个新事务呢?
有以下几种传播行为属性值
我们重点关注前两个:
REQUIRED:【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW:需要新事务,无论有无,总是创建新事务
我们还是通过一个案例来引入propagation属性值。
我们需要定义以下内容:
1.pojo包下定义实体类DeptLog.java
package com.gjw.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
2.service层定义接口DeptLogService和实现类DeptLogServiceImpl.java
接口DeptLogService
package com.gjw.service;
import com.gjw.pojo.DeptLog;
public interface DeptLogService {
void insert(DeptLog deptLog);
}
实现类DeptLogServiceImpl.java,该类中的insert()方法也是一个事务。
package com.gjw.service.impl;
import com.gjw.mapper.DeptLogMapper;
import com.gjw.pojo.DeptLog;
import com.gjw.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
3.DeptLogMapper接口:操作数据库,将日志内容记录在数据库表中
package com.gjw.mapper;
import com.gjw.pojo.DeptLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time, description) VALUES (#{createTime},#{description})")
void insert(DeptLog deptLog);
}
定义完成后,我们在DeptServiceImpl.java中的deleteById方法中调用操作日志的代码逻辑。因为解散部门时,不管成功还是失败,都要记录操作日志,因此要将日志操作存放在finally代码块中。我们使用try/finally来组合代码。在try中执行删除部门及相关员工的操作,在finally中执行日志操作。
package com.gjw.service.impl;
import com.gjw.mapper.DeptLogMapper;
import com.gjw.mapper.DeptMapper;
import com.gjw.mapper.EmpMapper;
import com.gjw.pojo.Dept;
import com.gjw.pojo.DeptLog;
import com.gjw.service.DeptLogService;
import com.gjw.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
@Override
public List<Dept> list() {
return deptMapper.list();
}
// @Transactional(rollbackFor = Exception.class) // spring事务管理 方法开始执行之前开启事务,方法执行完毕之后提交事务,方法运行过程中出现异常会回滚事务 rollbackFor:定义异常出现后回滚的类型,Exception.class表示不论出现任何异常均回滚事务,而默认只出现运行时异常才回滚事务
@Transactional
@Override
public void deleteById(Integer id) throws Exception {
// 根据部门id删除部门,同时也要删除部门下的员工
try {
deptMapper.deleteById(id);
int a = 1/0;
empMapper.deleteByDeptId(id);
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("删除id为"+id+"的部门");
deptLogService.insert(deptLog);
}
}
@Override
public void add(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}
@Override
public Dept getById(Integer id) {
return deptMapper.getById(id);
}
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}
接下来我们启动服务来执行删除部门的操作。此时因为1/0会导致运行时错误被捕捉到。接着我们刷新表结构,发现表结构中没有任何数据。该日志并没有被记录。这与我们的要求:“不论成功还是失败,都要记录操作日志”这一要求相违背,这是为什么呢?
我们来看一下控制台中输出的日志:
当前代码中的deleteById代码块执行时,作为一个事务,会首先执行删除部门的操作,接着遇到异常,删除部门下的员工这一代码将不会执行。但是finally中的代码块将会执行。而finally中的代码块中的insert操作也是一个事务,这就涉及到了两个事务之间的调用,也就是传播行为的问题了。在控制台日志当中输出:
Participating in existing transaction,也就是说参与到了现存的事务当中了。也就是说:insert这个后执行的事务参与到了deleteById这个先执行的事务当中了。也就是说deleteById和insert共用一个事务。然后便执行insert语句成功插入一条记录,但是由于出现了除以零的异常,
因此deleteById事务要回滚,又因为共用一个事务,因此insert也跟着回滚了,这就导致了在数据库中没有任何日志记录。
这是因为我们没有配置propagation属性,那么默认就是REQUIRED,需要事务,有则加入,无则创建新事务。
三.如何解决?
既然要解决这个问题,很明显的方法就是将这两个事务分开,使得不管deleteById()事务执行成功与否,insert()事务都将执行。那么就要使得insert()事务在执行时另开一个新的事务,而不管原来的事务是否存在。我们就要在insert()方法的@Transactional注解中加入propagation属性并指定属性值为REQUIRES_NEW。
package com.gjw.service.impl;
import com.gjw.mapper.DeptLogMapper;
import com.gjw.pojo.DeptLog;
import com.gjw.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}