第13章 Springboot事务配置和实践

1,109 阅读11分钟

任务描述

任务要求

使用IDEA开发工具构建一个项目多模块工程。study-springboot-chapter07学习关于Springboot事务有关知识点

  1. 基于study-springboot工程,新建一个Maven空项目,坐标groupId(com.cbitedu)、artifactId(study-springboot-chapter07),其他默认
  2. 继承study-springboot工程依赖
  3. 详细学习Spring MybatisPlus,提供的工具类来完成测试。

任务收获

  1. 如何集成第三方持久化技术Spring MybatisPlus
  2. 如何引入MySQL数据库依赖
  3. Spring Boot中整合Spring MybatisPlus完成关系型数据库的增删改查操作
  4. 学会使用JUnit完成单元测试
  5. 掌握Spring MybatisPlus在项目中的应用
  6. 掌握Spring事务的使用

任务准备

环境要求

  1. JDK1.8+
  2. MySQL8.0.27+
  3. Maven 3.6.1+
  4. 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

任务实施

什么是事务?

我们在开发企业应用时,通常业务人员的一个操作实际上是对数据库读写的多步操作的结合。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作的数据并不可靠,如果要让这个业务正确的执行下去,通常有实现方式:

  1. 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面要做的业务逻辑
  2. 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,带问题修复之后,重新执行原来的业务逻辑

事务就是针对上述方式2的实现。事务,一般是指要做的或所做的事情,就是上面所说的业务人员的一个操作(比如电商系统中,一个创建订单的操作包含了创建订单、商品库存的扣减两个基本操作。如果创建订单成功,库存扣减失败,那么就会出现商品超卖的问题,所以最基本的最发就是需要为这两个操作用事务包括起来,保证这两个操作要么都成功,要么都失败)。

这样的场景在实际开发过程中非常多,所以今天就来一起学习一下Spring Boot中的事务管理如何使用!

一、事务介绍

  1. 使用事务,我们只需要在需要事务的类或方法上使用@Transactional 注解即可,当注解在类上的时候意味着此类的所有public方法都是开启事务的。被注解的方法都成为一个事务整体,同一个事务内共享一个数据库连接,所有操作同时发生。如果在事务内部执行过程中发生了异常,则事务整体会自动进行回滚。
  2. 任何的RuntimeExcetipn、Error将触发回滚,任何的checkedException不触发回滚,@Transactional(rollbackFor=Exception.class)或者throw new RuntimeException()就可以解决checkedException不触发回滚。
  3. 当用作方法上时,该方法所在类上的注解将失效。
  4. 只有来自外部的方法调用才会引起事务行为,类内部方法调用本类内部的其他方法并不会引起事务行为。
  5. 在入口类使用注解@EnableTransactionManagement开启事务。

二、事务并发执行带来的问题 

  1. 脏读:一个事务读到了另一个未提交事务修改过的数据
  2. 幻读:一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。
  3. 不可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每次对该数据进行一次修改并提交后,该事务都能查询得到最新值。

三、事务的特性

原子性、一致性、隔离性和持久性,简称为事务的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-jdbcspring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

仓库地址:gitee.com/ossbar/stud…

模块: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 事务,异常如何处理