• 【Spring】编程式事务的应用场景理解


    前言

    我们经常在使用Spring全家桶开发JavaEE项目的时候,一想到事务就会习惯性的使用声明式注解@Transactional,由Spring框架帮你做AOP实现事务的回滚,但是声明式事务恰恰比较方便,所以有些场景下并不好用,接下来我来举一个例子,看大家有没有遇到过类似的需求场景。

    场景复现

    说有这么两张表,业主表房屋表。关系是 一个业主可以有多个房屋房产登记,一对多的关系。

    表结构
    --业主表
    CREATE TABLE `person` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `name` varchar(100) DEFAULT NULL COMMENT '名称',
      `age` int(3) DEFAULT '0' COMMENT '年龄',
      `address` varchar(100) DEFAULT NULL COMMENT '地址',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
    
    --房屋表
    CREATE TABLE `home` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `home_name` varchar(30) DEFAULT NULL COMMENT '房屋名称',
      `person_id` bigint(20) DEFAULT NULL COMMENT '所属业主',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    实体类
    /**
    业主实体
    */
    @Data
    @TableName("person")
    public class PersonEntity {
    
        @TableId(value = "id",type = IdType.AUTO)
        private Long id;
    
        private String name;
    
        private Integer age;
    
        private String address;
    }
    
    /**
    房屋实体
    */
    @Data
    @TableName("home")
    public class HomeEntity {
    
        @TableId(value = "id",type = IdType.AUTO)
        private Long id;
    
        @TableField("home_name")
        private String homeName;
    
        @TableField("person_id")
        private Long personId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    需求

    现在要求有一个添加业主信息的接口,支持将批量的业主信息和相关的房产信息录入到系统中,然后返回的数据结构中要告知调用者哪些成功哪些失败了

    实现

    根据上面的需求,我们涉及多表的插入或者说循环插入表的环境都要先想到事务,这里第一前提是对单个业主来说,业主信息跟房屋信息一定是要么都成功要么都失败,不能说业主信息录入了,房屋信息录入失败,而业主信息还留在上面(这里不讨论业务容错性,就是失败了都不能留有失败业主的记录在上面),这时候脑子里会有以下这个设计接口的构思。

    请求参数对象

    @Data
    public class PersonRequest {
    
        //名称
        private String name;
    
        //年龄
        private int age;
    
        //地址
        private String address;
    
        //多个房屋信息
        private List<HomeEntity> homeList;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Controller层

    @RequestMapping("person")
    @RestController
    public class PersonController {
    
        @Autowired
        TestService testService;
    
        @PostMapping("person-v3")
        public DefaultResponse addPersonV3(@RequestBody List<PersonRequest> request) {
            testService.addPersonV3(request);
            return DefaultResponse.DEFAULT_RESPONSE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Service层

    这里就展示实现类的addPersonV2方法代码

    @Transactional
    @Override
    public void addPersonV3(List<PersonRequest> request) {
        request.forEach(item->{
    
            //插入业主信息表中
            PersonEntity personInsert = new PersonEntity();
            personInsert.setName(item.getName());
            personInsert.setAge(item.getAge());
            personInsert.setAddress(item.getAddress());
            this.save(personInsert);
    
            // 插入房屋表
            if (CollectionUtils.isNotEmpty(item.getHomeList())) {
                item.getHomeList().forEach(home->{
                    HomeEntity homeEntity = new HomeEntity();
                    homeEntity.setHomeName(home.getHomeName());
                    homeEntity.setPersonId(personInsert.getId());
                    this.homeMapper.insert(homeEntity);
                });
            }
    
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这样子的代码只保证了事务,一旦报错都会回滚,满足不了能提示给用户哪些成功哪些不成功的数据结构。 这时候我们又会想到下面这个用try..catch方式将业务逻辑包起来,然后用一个返回对象记录成功跟失败的信息给前端。

    Vo

    /**
    *@Description   业主添加的返回视图
    *@Author        wengzhongjie
    *@Date          2022/12/1 9:39
    *@Version
    */
    @Data
    public class PersonResponse {
    
        //记录录入成功的名字
        private List<String> success=new ArrayList<>();
    
        //记录录入失败的名字
        private List<String> fail=new ArrayList<>();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Controller

    @RequestMapping("person")
    @RestController
    public class PersonController {
    
        @Autowired
        TestService testService;
    
        @PostMapping("person-v3")
        public PersonResponse addPersonV3(@RequestBody List<PersonRequest> request) {
            return testService.addPersonV3(request);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Service

    @Transactional
    @Override
    public PersonResponse addPersonV3(List<PersonRequest> request) {
        PersonResponse response = new PersonResponse();
        request.forEach(item->{
    
            try{
                //插入业主信息表中
                PersonEntity personInsert = new PersonEntity();
                personInsert.setName(item.getName());
                personInsert.setAge(item.getAge());
                personInsert.setAddress(item.getAddress());
                this.save(personInsert);
    
                // 插入房屋表
                if (CollectionUtils.isNotEmpty(item.getHomeList())) {
                    item.getHomeList().forEach(home->{
                        HomeEntity homeEntity = new HomeEntity();
                        homeEntity.setHomeName(home.getHomeName());
                        homeEntity.setPersonId(personInsert.getId());
                        this.homeMapper.insert(homeEntity);
                    });
                }
                //记录成功的
                response.getSuccess().add(item.getName());
            }catch (Exception e){
                //记录失败的
                response.getFail().add(item.getName());
            }
        });
        return response;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这样子看着好像解决了返回的需求,但是其实这时候这个@Transactional已经没啥用了,因为使用了try...catch之后没有继续向外抛出,对于Spring来说,他是觉得你没有出错的。你看着感觉好像没事,但是如果代码出现在插入房屋表的时候出现错误怎么办? 业主信息也不会被回滚,这时候其实就出现了脏数据。如图所示。

    请求数据

    [
      {
      "name": "田七",
      "age": 1,
      "address": "福建福州",
      "homeList": [{
        "homeName": "福州仓山区某某小区3单元501"
      },{
        "homeName": "福州台江区区某某小区5单元101"
      },{
        "homeName": "福州鼓楼区某某小区1单元901"
      }]
    },
    {
      "name": "周八",
      "age": 1,
      "address": "福建莆田",
      "homeList": [{
        "homeName": "莆田城厢区某某小区3单元501"
      },{
        "homeName": "莆田秀屿区某某小区5单元101"
      },{
        "homeName": "莆田涵江区某某小区1单元901"
      }]
    },
    {
      "name": "郑九",
      "age": 1,
      "address": "福建福清",
      "homeList": [{
        "homeName": "福清西区某某小区3单元501"
      },{
        "homeName": "福清北区区某某小区5单元101"
      },{
        "homeName": "福清东北区某某小区1单元901"
      }]
    }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    这里故意设置成3个业主中间的周八业主在插入房屋表时报错,会发现周八的房屋信息没有插入进去,但是业主信息却被插入了,这还是稍微理想的状态,如果周八有多个房屋信息,在遍历后面几个房屋信息的时候出错,就连前面录入的房屋信息也会出现在表里,这是肯定不被允许的。但是一旦加了异常抛出,又会被全部回滚,这时候手动回滚,也就是声明式事务出场了。

    我们只要在关键的业务代码位置加上开启事务提交事务回滚事务即可。

    public void method(){
        try{
            //开启事务
            
            //=====业务代码=====START
            //todo 
            //=====业务代码=====END
            
            //提交事务
        }catch(Exception e){
            //回滚事务
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这样子之后,事务由我们自己控制,我们只要在周八报错的时候,给他回滚一下,这样就不会出现脏数据了。但是我们再思考一下当初我们为什么从编程式事务变成了声明式事务,不就是因为方便,写这些开启、提交、回滚实在是太烦了。所以Spring给我们提供了一个TransactionTemplate类帮助我们更方便的简写代码。

    在这里插入图片描述

    通过上面的execute方法我们来实现需求

    @Override
    public PersonResponse addPersonV3(List<PersonRequest> request) {
        PersonResponse response = new PersonResponse();
        request.forEach(item->{
    
            try{
                transactionTemplate.execute(status -> {
                    //插入业主信息表中
                    PersonEntity personInsert = new PersonEntity();
                    personInsert.setName(item.getName());
                    personInsert.setAge(item.getAge());
                    personInsert.setAddress(item.getAddress());
                    this.save(personInsert);
    
                    // 插入房屋表
                    if (CollectionUtils.isNotEmpty(item.getHomeList())) {
                        item.getHomeList().forEach(home -> {
                            //如果是周八 就模拟出错
                            if ("周八".equals(item.getName())) {
                                int i = 1 / 0;
                            }
                            HomeEntity homeEntity = new HomeEntity();
                            homeEntity.setHomeName(home.getHomeName());
                            homeEntity.setPersonId(personInsert.getId());
                            this.homeMapper.insert(homeEntity);
                        });
                    }
                    return Boolean.TRUE;
                });
                response.getSuccess().add(item.getName());
            }catch (Exception e){
                response.getFail().add(item.getName());
            }
    
        });
        return response;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    再次请求接口看一下测试结果

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    所以我觉得编程式事务还是有使用场景的,而且Spring还提供了一个很方便的方法灰常的不错。

  • 相关阅读:
    基于Python+tkinter实现一个简易计算器桌面软件
    类加载器及反射简单笔记
    极简UVM RAL示例(PART3--后门访问,PART4--内建ral sequence)
    第八部分:JSP
    秒杀哲学家问题的万能思路
    Unity中Shader光照模型Phong的实现
    [附源码]SSM计算机毕业设计智慧教学平台JAVA
    SpringBoot 10 登录功能和登录拦截器
    Windows 远程桌面连接方法及远程桌面控制软件推荐
    FreeBASIC通过Delphi7 DLL调用MS SOAP使用VB6 Webservice
  • 原文地址:https://blog.csdn.net/javaboyweng/article/details/128127693