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

news/2024/7/3 0:15:59

前言

我们经常在使用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;
实体类
/**
业主实体
*/
@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;
}
需求

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

实现

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

请求参数对象

@Data
public class PersonRequest {

    //名称
    private String name;

    //年龄
    private int age;

    //地址
    private String address;

    //多个房屋信息
    private List<HomeEntity> homeList;
}

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;
    }
}

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);
            });
        }

    });
}

这样子的代码只保证了事务,一旦报错都会回滚,满足不了能提示给用户哪些成功哪些不成功的数据结构。 这时候我们又会想到下面这个用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<>();
}

Controller

@RequestMapping("person")
@RestController
public class PersonController {

    @Autowired
    TestService testService;

    @PostMapping("person-v3")
    public PersonResponse addPersonV3(@RequestBody List<PersonRequest> request) {
        return testService.addPersonV3(request);
    }
}

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;
}

这样子看着好像解决了返回的需求,但是其实这时候这个@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"
  }]
}
]

在这里插入图片描述

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

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

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

public void method(){
    try{
        //开启事务
        
        //=====业务代码=====START
        //todo 
        //=====业务代码=====END
        
        //提交事务
    }catch(Exception e){
        //回滚事务
    }
}

这样子之后,事务由我们自己控制,我们只要在周八报错的时候,给他回滚一下,这样就不会出现脏数据了。但是我们再思考一下当初我们为什么从编程式事务变成了声明式事务,不就是因为方便,写这些开启、提交、回滚实在是太烦了。所以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;
}

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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


http://lihuaxi.xjx100.cn/news/252669.html

相关文章

【青少年编程(第26周)】一下子多了很多事!

2021年08月15日&#xff08;周日&#xff09;晚20:00我们在青少年编程竞赛交流群开展了第二十六次直播活动。我们直播活动的主要内容如下&#xff1a; 首先&#xff0c;我们奖励了上周测试超过60分的小朋友。 其次&#xff0c;介绍了Datawhale青少年组队学习活动的规划&#x…

ranlib

ranlib 产生archive文件的索引 The index lists each symbol defined by a member of an archive that is a relocatable object file.

【MATLAB】数组运算

&#xff08;这里这列举笔者不熟悉的&#xff0c;容易忘的数组运算&#xff09; 1、数组的转置 >> a[1 2 3 4 5 6 7]a 1 2 3 4 5 6 7>> bab 1234567 2、对数组的赋值 >> a([1 4])[0 0]a 0 2 3 0 5 6 73、注…

“Git 是我用过最笨重的软件”!喷完 C++ 喷 Git,这位 Azure CTO 到底何许人也?...

作者 | 辛晓亮出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 几天前呼吁停用 C 改换 Rust 的微软 Azure CTO 这次又在推特上疯狂吐槽 Git&#xff0c;Mark Russinovich 称&#xff1a;“Git 是我使用过的所有软件中界面最不直观、最笨重的&#xff0c;它让我抓狂…

connect() failed (111: Connection refused) while connecting to upstream, cli

php-fpm没有运行 执行如下命令查看是否启动了php-fpm&#xff0c;如果没有则启动你的php-fpm即可 netstat -ant | grep 9000没有运行为空&#xff0c;有运行显示 tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 启动方法 sudo /usr/loca…

教育部办公厅关于2020-2021学年面向中小学生的全国性竞赛活动名单的公示

来源&#xff1a; http://www.moe.gov.cn/jyb_xxgk/s5743/s5745/202007/t20200701_469571.html 教育部办公厅关于2020-2021学年面向中小学生的全国性竞赛活动名单的公示 根据《教育部办公厅印发<关于面向中小学生的全国性竞赛活动管理办法&#xff08;试行&#xff09;&g…

【MATLAB】几种特殊矩阵,Hilbert矩阵,Toeplitz矩阵,Vandermonde矩阵......

1、Hadamard矩阵 Hadamard矩阵是由1和-1元素构成的且满足Hn*Hn’nI&#xff08;这里Hn’为Hn的转置&#xff0c;I为单位方阵&#xff09;n阶方阵。 >> hadamard(4)ans 1 1 1 11 -1 1 -11 1 -1 -11 -1 -1 12、Hankel矩阵&#…

微信自研NLP大规模语言模型WeLM:零/少样本即可完成多种NLP任务

近日&#xff0c;微信AI推出自研NLP大规模语言模型WeLM &#xff0c;该模型是一个尺寸合理的中文模型&#xff0c;能够在零样本以及少样本的情境下完成包多语言任务在内的多种NLP任务。同时&#xff0c;微信AI团队也提供了WeLM的体验网页和API接口&#xff0c;感兴趣的用户可前…