前言
我们经常在使用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还提供了一个很方便的方法灰常的不错。