【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;
实体类
/**
业主实体
*/
@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还提供了一个很方便的方法灰常的不错。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰肥啊

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值