任务描述
任务要求
使用IDEA开发工具构建一个项目多模块工程。study-springboot-chapter07学习关于Springboot事务有关知识点
- 基于study-springboot工程,新建一个Maven空项目,坐标groupId(com.cbitedu)、artifactId(study-springboot-chapter07),其他默认
- 继承study-springboot工程依赖
- 详细学习Spring MybatisPlus,提供的工具类来完成测试。
任务收获
- 如何集成第三方持久化技术Spring MybatisPlus
- 如何引入MySQL数据库依赖
- Spring Boot中整合Spring MybatisPlus完成关系型数据库的增删改查操作
- 学会使用JUnit完成单元测试
- 掌握Spring MybatisPlus在项目中的应用
- 掌握Spring事务的使用
任务准备
环境要求
- JDK1.8+
- MySQL8.0.27+
- Maven 3.6.1+
- IDEA/VSCode
数据库准备
创建数据库platform,并创建部门表、雇员表。
-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
工程目录要求
新建一个空的Maven项目:study-springboot-chapter07
任务实施
什么是事务?
我们在开发企业应用时,通常业务人员的一个操作实际上是对数据库读写的多步操作的结合。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作的数据并不可靠,如果要让这个业务正确的执行下去,通常有实现方式:
- 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面要做的业务逻辑
- 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,带问题修复之后,重新执行原来的业务逻辑
事务就是针对上述方式2的实现。事务,一般是指要做的或所做的事情,就是上面所说的业务人员的一个操作(比如电商系统中,一个创建订单的操作包含了创建订单、商品库存的扣减两个基本操作。如果创建订单成功,库存扣减失败,那么就会出现商品超卖的问题,所以最基本的最发就是需要为这两个操作用事务包括起来,保证这两个操作要么都成功,要么都失败)。
这样的场景在实际开发过程中非常多,所以今天就来一起学习一下Spring Boot中的事务管理如何使用!
一、事务介绍
- 使用事务,我们只需要在需要事务的类或方法上使用@Transactional 注解即可,当注解在类上的时候意味着此类的所有public方法都是开启事务的。被注解的方法都成为一个事务整体,同一个事务内共享一个数据库连接,所有操作同时发生。如果在事务内部执行过程中发生了异常,则事务整体会自动进行回滚。
- 任何的RuntimeExcetipn、Error将触发回滚,任何的checkedException不触发回滚,@Transactional(rollbackFor=Exception.class)或者throw new RuntimeException()就可以解决checkedException不触发回滚。
- 当用作方法上时,该方法所在类上的注解将失效。
- 只有来自外部的方法调用才会引起事务行为,类内部方法调用本类内部的其他方法并不会引起事务行为。
- 在入口类使用注解@EnableTransactionManagement开启事务。
二、事务并发执行带来的问题
- 脏读:一个事务读到了另一个未提交事务修改过的数据
- 幻读:一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。
- 不可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每次对该数据进行一次修改并提交后,该事务都能查询得到最新值。
三、事务的特性
原子性、一致性、隔离性和持久性,简称为事务的ACID特性。
四、事务隔离级别
@Transactional(isolation = Isolation.DEFAULT)
DEFAULT :默认值(也是SpringBoot的隔离级别默认值),表示使用底层数据库的默认隔离级别。大部分数据库为READ_COMMITTED(MySql默认隔离级别为REPEATABLE_READ)
READ_UNCOMMITTED :一个事务可以读取另一个事务修改但还没有提交的数据。
READ_COMMITTED :一个事务只能读取另一个事务已经提交的数据。
REPEATABLE_READ :一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。
SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。
五、事务传播方式
@Transactional(propagation = Propagation.REQUIRED)
PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。
PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRESNEW:新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中,如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
六、SpringBoot中事务控制失效的原因
1.检查你方法是不是public的(只有Public方法才能开启事务)
2.检查你的异常类型是不是unchecked异常(默认Error和RuntimeException事务回滚)。
3.检查异常是不是被catch住了(当异常被捕获后,并且没有再抛出,那么事务是不会回滚的。)
4.检查类有没有被Spring管理(方法被标注了@Transactional,但是类没有注解,没有被Spring管理也不会生效)
5.代码写错了
6 .数据库引擎不支持事务(其 MyISAM 引擎是不支持事务操作的)
七、Transactional()中的参数说明
1、 propagation:事务传播行为
2、 isolation:事务隔离级别
3、 timeout:超时时间,事务需要在一定的时间内提交,不提交则进行回滚;默认值是-1,表示无时间限制
4、 readOnly:是否只读
(1).默认值为false,表示可以查询,可以增删改;
(2).如果设置为true,只能查询操作。
5、rollbackFor:回滚,设置出现哪些异常进行事务回滚。
6、noRollbackFor:不回滚,设置出现哪些异常不进行回滚。
快速入门
在Spring Boot中,当我们使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。
模块:study-springboot-chapter07
任务实施步骤如下:
1、study-springboot-chapter07模块的pom.xml中引入MyBatis plus的Starter以及MySQL Connector依赖和lombok第三方框架,具体如下:
如果遇到下载包Jar包不成功:可以删除本地仓库下所有的第三方包或者搜索lastupdate字样,删除后再重新下载
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2、新建application.yml配置mysql的连接配置和mybatis-plus
MyBatisPlus 默认扫描的是类路径下的 resources/mapper目录,所以我们直接将 Mapper 配置文件放在该目录下就没有任何问题,可以省略配置,可如果不是这个目录,我们就需要进行配置
mybatis-plus:
mapper-locations: classpath:mybatis/mapper/*.xml
#服务配置
server:
port: 81
# #设置日志相关打印sql 语句
logging:
level:
top.com.cbitedu.springboot: debug
#关闭运行日志图标(banner)
spring:
datasource:
url: jdbc:mysql://localhost:3306/platform?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# # 指定全局配置文件位置
config-location: classpath:mybatis/mybatis-config.xml
# # 指定sql映射文件位置
mapper-locations: classpath:mybatis/mapper/*.xml
新建日志文件logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台打印日志的相关配置 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%level] - %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/mybatisplus_info.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/mybatisplus_info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/mybatisplus_error.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/mybatisplus_error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
<appender-ref ref="file" />
<appender-ref ref="error" />
</root>
</configuration>
3、完成任务准备中的数据库工作,创建表
分别在com.cbitedu.springboot下创建软件包
- 实体类:entity
- 接口层:mapper
- 服务层:service
- 控制台:controller
5、创建department表的映射对象Department( com.cbitedu.springboot.entity )
package com.cbitedu.springboot.entity;
import lombok.Data;
@Data
public class Department {
private Integer id;
private String departmentName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", departmentName='" + departmentName + ''' +
'}';
}
}
5、创建employee表的映射对象Employee( com.cbitedu.springboot.entity )
package com.cbitedu.springboot.entity;
import lombok.Data;
@Data
public class Employee {
private Integer id;
private String lastName;
private Integer gender;
private String email;
private Integer dId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getdId() {
return dId;
}
public void setdId(Integer dId) {
this.dId = dId;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastName='" + lastName + ''' +
", gender=" + gender +
", email='" + email + ''' +
", dId=" + dId +
'}';
}
}
6、编写Mapper的XML文件:EmployeeMapper.xml(resources/mybatis/mapper目录)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cbitedu.springboot.dao.EmployeeMapper">
<select id="getEmpById" resultType="com.cbitedu.springboot.entity.Employee">
select * from employee where id=#{id}
</select>
<insert id="insertEmp">
insert into employee(lastName,email,gender,d_id)values (#{lastName},#{email},#{gender},#{dId})
</insert>
</mapper>
7、创建接口: EmployeeMapper(xml实现)和DepartmentMapper(注解型实现)
注意:使用mybatis-plus的公共接口,必须继承BaseMapper,提供了一系列常用的接口方法
package com.cbitedu.springboot.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cbitedu.springboot.entity.Employee;
public interface EmployeeMapper extends BaseMapper<Employee> {
public Employee getEmpById(Integer id);
public void insertEmp(Employee employee);
}
DepartmentMapper基于注解型实现
package com.cbitedu.springboot.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cbitedu.springboot.entity.Department;
import org.apache.ibatis.annotations.*;
@Mapper
public interface DepartmentMapper extends BaseMapper<Department> {
@Select("select * from department where id=#{id}")
public Department getDeptById(Integer id);
@Delete("delete from department where id=#{id}")
public int deleteById(Integer id);
@Options(useGeneratedKeys = true,keyColumn = "id")
@Insert("insert into department (departmentName) values(#{departmentName})")
public int insertDept(Department department);
@Update("update department set departmentName = #{departmentName} where id =#{id}")
public int updateDept(Department department);
}
8、创建服务层接口IEmployeeService
和实现类EmployeeServiceImpl
注意:IEmployeeService继承IService接口
package com.cbitedu.springboot.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cbitedu.springboot.entity.Employee;
public interface IEmployeeService extends IService<Employee> {
public Employee getEmpById(Integer id);
public void insertEmp(Employee employee);
public void addEmp(Employee employee);
}
IEmployeeService接口实现类EmployeeServiceImpl
package com.cbitedu.springboot.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cbitedu.springboot.dao.EmployeeMapper;
import com.cbitedu.springboot.entity.Department;
import com.cbitedu.springboot.entity.Employee;
import com.cbitedu.springboot.service.IDepartmentService;
import com.cbitedu.springboot.service.IEmployeeService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
@Resource
private EmployeeMapper employeeMapper;
@Resource
private IDepartmentService departmentService;
@Override
public Employee getEmpById(Integer id) {
return employeeMapper.getEmpById(id);
}
@Override
@Transactional
public void insertEmp(Employee employee) {
Department department = new Department();
department.setDepartmentName("测试事务");
employeeMapper.insertEmp(employee);
//1、EmployeeServiceImpl(简称A)中调用DepartmentServiceImpl(简称B)中的方法
// 当A和B中的方法事务都是REQUIRED,B中抛异常回滚,A中catch住B的异常,A中事务还是会回滚,
// 并抛出:UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
//
try {
departmentService.insertDept(department);
} catch (Exception e) {
System.out.println(e);
}
}
@Override
@Transactional
public void addEmp(Employee employee) {
employee.setEmail("zzzzzz");
employeeMapper.insertEmp(employee);
int i = 1 / 0;
}
}
9、创建服务层接口IDepartmentService
和实现类DepartmentServiceImpl
注意:IDepartmentService继承IService接口
package com.cbitedu.springboot.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cbitedu.springboot.entity.Department;
public interface IDepartmentService extends IService<Department> {
public Department getDeptById(Integer id);
public int deleteById(Integer id);
public int insertDept(Department department);
public int updateDept(Department department);
}
IDepartmentService接口实现类DepartmentServiceImpl
package com.cbitedu.springboot.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cbitedu.springboot.dao.DepartmentMapper;
import com.cbitedu.springboot.entity.Department;
import com.cbitedu.springboot.service.IDepartmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Department> implements IDepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Override
public Department getDeptById(Integer id) {
return departmentMapper.getDeptById(id);
}
@Override
public int deleteById(Integer id) {
return departmentMapper.deleteById(id);
}
@Override
public int insertDept(Department department) {
int i = departmentMapper.insertDept(department);
int a = 1 / 0;
return i;
}
@Override
public int updateDept(Department department) {
return 0;
}
}
10、创建Spring Boot主类,并扫描到Mybatis接口
package com.cbitedu.springboot;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.cbitedu.springboot.dao")
public class StudySpringbootChapter07Application{
private static final Logger logger = LoggerFactory.getLogger(StudySpringbootChapter07Application.class);
public static void main(String[] args) {
// 启动 Spring Boot 应用
SpringApplication.run(StudySpringbootChapter07Application.class, args);
}
}
11、创建单元测试:SpringBootMybatisApplicationTests事务下完成:测试新增、修改、删除业务操作。
package com.cbitedu.springboot.web;
import com.cbitedu.springboot.entity.Employee;
import com.cbitedu.springboot.service.IDepartmentService;
import com.cbitedu.springboot.service.IEmployeeService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootMybatisApplicationTests {
@Autowired
private IEmployeeService employeeService;
@Autowired
private IDepartmentService departmentService;
@Test
public void contextLoads() {
//设置员工名称长度超过数据库长度,则插入员工信息失败,同样部门表插入数据也失败
// System.out.println(employeeService.getEmpById(1));
Employee employee = new Employee();
employee.setdId(2);
employee.setLastName("张三");
employee.setEmail("zhangs@qq.com");
employeeService.insertEmp(employee);
}
@Test
public void testFailureInsert() {
//设置员工名称长度超过数据库长度,则插入员工信息失败,同样部门表插入数据也失败
// System.out.println(employeeService.getEmpById(1));
Employee employee = new Employee();
employee.setdId(2);
employee.setLastName("张三zhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.comzhangs@qq.com");
employee.setEmail("zhangs@qq.com");
employeeService.insertEmp(employee);
}
}
实验实训
1、Springboot 事务,异常如何处理