文章目录
- 前言
- 一、service层
- 1. 提交学生信息
- 2. 申请借阅资格
- 3. 重新提交
- 4. 事务
- 二、web层 StudentController
- 三、测试
- 最后
前言
通过上文,我们实现了【学生入驻】的第一个API:查询学生信息,接下来的流程通常如下图:如果学生未入驻,将提示学生填写信息,申请借阅资格(借阅证),这也正是本文要实现的需求!
在【数据库设计 --MySQL】时曾做过业务分析:提交学生信息(插入student表)、申请借阅证(插入qualification表),这两个SQL是一步操作,也就是原子操作,所以会用到数据库事务!
在【7.8】曾讲过声明式事务@Transactional,但有的时侯仍需要 编程式事务,所以本文将结合实战场景,帮助你正确理解编程式事务和声明式事务!
一、service层
StudentService
定义方法如下(studentBO是提交的学生信息):
/**
* 提交学生信息,并申请借阅证
**/
void apply(StudentBO studentBO);
StudentServiceImpl
的对应的空实现:
@Override
public void apply(StudentBO studentBO) {
}
1. 提交学生信息
先看看数据库表设计:
小插曲:一个字段小调整
对于is_approved
字段,考虑到后面更方便查询,所以决定修改为:是否申请通过(0-待审核 1-通过 2-未通过)。
修改方法:
- 修改
generatorConfig.xml
的is_approved
配置:将javaType从Boolean
修改为Integer
,并双击generate重新生成,如下图:
- 生成以后,请确认Student和StudentExample类已被修改。
- 然后,再手动修改
StudentBO
类的private Integer isApproved;
回归主题,由此,提交学生信息的实现如下:
@Override
public void apply(StudentBO studentBO) {
// 1. 提交学生信息
// 初始化基础数据
studentBO.setIsApproved(ExamineEnum.TO_BE_EXAMINE.getCode());
studentBO.setIsFrozen(Boolean.FALSE);
studentBO.setGmtCreate(new Date());
studentBO.setGmtModified(new Date());
Student po = CopyUtils.copy(studentBO, Student::new);
studentMapper.insertSelective(po);
}
因为大部分审核同样都是3种状态,所以这里将审核状态
封装成了一个公共枚举类:
package org.tg.book.common.enums;
/**
* 审核状态枚举
**/
public enum ExamineEnum {
TO_BE_EXAMINE(0, "待审核"),
APPROVED(1, "审核通过"),
REJECTED(2, "驳回"),
;
Integer code;
String msg;
ExamineEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return this.code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
2. 申请借阅资格
我们设计的是可以多次申请,并且每次都会保存申请记录!
首先注入QualificationMapper
:
@Autowired
private QualificationMapper qualificationMapper;
对于插入qualification表
,只需要提供学生id、申请状态(0-待审核 1-通过 2-未通过),实现如下:
// 2. 申请借阅资格
Qualification qualification = new Qualification();
qualification.setStudentId(student.getId());
qualification.setStatus(ExamineEnum.TO_BE_EXAMINE.getCode());
qualification.setGmtCreate(new Date());
qualification.setGmtModified(new Date());
qualificationMapper.insertSelective(qualification);
3. 重新提交
前两步的实现实际还缺少重新提交
的情况,所以完整的流程应该先查询学生信息
,
- 如果不存在,则直接走前2步提交;
- 如果存在,还需要校验是否已通过,如果已通过是不可以重新提交,否则重新提交。
将查询
和两步校验
写在前面,代码如下:
// 查询学生
Student student = studentMapperExt.selectByUserId(studentBO.getUserId());
if (student != null) {
// 如果已审核通过,不可以重新提交
Assert.ifTrue(ExamineEnum.APPROVED.getCode().equals(student.getIsApproved()), "已审核通过,请勿重新提交!");
// 如果未通过, 看是否有待审核的借阅记录
QualificationExample example = new QualificationExample();
example.createCriteria().andStudentIdEqualTo(student.getId())
.andStatusEqualTo(ExamineEnum.TO_BE_EXAMINE.getCode());
long count = qualificationMapper.countByExample(example);
Assert.ifTrue(count > 0, "已提交待审核,请勿重新提交!");
}
提交学生信息,同样需要支持已存在情况,无则insert,有则update
,所以改造代码如下:
// 1. 提交学生信息
studentBO.setIsApproved(ExamineEnum.TO_BE_EXAMINE.getCode());
if (student == null) {
// 初始化基础数据
studentBO.setIsFrozen(Boolean.FALSE);
studentBO.setGmtCreate(new Date());
studentBO.setGmtModified(new Date());
student = CopyUtils.copy(studentBO, Student::new);
studentMapper.insertSelective(student);
} else {
// 已存在,只拷贝新录入的赋值属性
CopyUtils.copyPropertiesIgnoreNull(studentBO, student);
studentMapper.updateByPrimaryKeySelective(student);
}
到这,整体流程上看起来已经OK了,但是,还差一点点,那就是事务
!
4. 事务
声明式事务 我们之前曾用过,非常易用,非常爽,在【7.8】已经详细说明过,只需要在方法上加注解:
@Transactional(rollbackFor = Exception.class)
所以,本文主要再说一下编程式事务!
纵所周知,事务虽好,但我们应尽可能避免长事务
,应尽可能给数据库减压
,所以事务的粒度应尽可能小,尽可能去除与事务无关的代码,能不开启则不必开启!
仔细思考,我们的场景正适合,我们真正需要事务的地方是【1. 提交学生信息】和【2. 申请借阅资格】,所以:
- 查询逻辑没必要开启事务
- 校验逻辑更必要开启事务,因为还可能回滚
这时如果想用声明式事务就不太合适了,因为@Transactional
的粒度是应用于方法
,如果仍要使用,需要拆分为两个方法,并且需要同类中两个方法调用,还要保证AOP注解生效!所以这就是 编程式事务的用武之地,因为它可以作用于代码块
,并且粒度更小,更灵活!
OK,那编程式事务 怎么用呢?
我推荐使用的是TransactionTemplate
,Spring为我们封装的非常易用的Template!它本身继承于TransactionDefinition
,内部注入了PlatformTransactionManager
,并且封装好了execute
泛型方法:回滚、提交都安排的明明白白,有兴趣可以看看流程的编排,所以咱们用它吧!
用法过于简单,TransactionTemplate
基本模板用法:
transactionTemplate.execute(action -> {
// 事务执行的代码。。。
// TODO 1。。。
// TODO 2。。。
// 最后根据需要返回,无返回值则return null
return null;
});
接着具体应用,首先注入TransactionTemplate
:
@Autowired
private TransactionTemplate transactionTemplate;
然后,只需要把【1. 提交学生信息】和【2. 申请借阅资格】作为action传入即可!
因为全部传入,会报错:Variable used in lambda expression should be final or effectively final
所以,我们将构建student的过程也可以提到事务外,然后将final student
再传入,完整实现代码如下:
// @Transactional(rollbackFor = Exception.class)
@Override
public void apply(StudentBO studentBO) {
// 查询学生
Student student = studentMapperExt.selectByUserId(studentBO.getUserId());
if (student != null) {
// 如果已审核通过,不可以重新提交
Assert.ifTrue(ExamineEnum.APPROVED.getCode().equals(student.getIsApproved()), "已审核通过,请勿重新提交!");
// 如果未通过, 看是否有待审核的借阅记录
QualificationExample example = new QualificationExample();
example.createCriteria().andStudentIdEqualTo(student.getId())
.andStatusEqualTo(ExamineEnum.TO_BE_EXAMINE.getCode());
long count = qualificationMapper.countByExample(example);
Assert.ifTrue(count > 0, "已提交待审核,请勿重新提交!");
}
// 初始化基础数据
studentBO.setIsApproved(ExamineEnum.TO_BE_EXAMINE.getCode());
if (student == null) {
studentBO.setIsFrozen(Boolean.FALSE);
studentBO.setGmtCreate(new Date());
studentBO.setGmtModified(new Date());
student = CopyUtils.copy(studentBO, Student::new);
} else {
// 已存在,只拷贝新录入的赋值属性
CopyUtils.copyPropertiesIgnoreNull(studentBO, student);
}
Student finalStudent = student;
transactionTemplate.execute(action -> {
// 1. 提交学生信息
if (finalStudent.getId() == null) {
studentMapper.insertSelective(finalStudent);
studentBO.setId(finalStudent.getId());
} else {
studentMapper.updateByPrimaryKeySelective(finalStudent);
}
// 2. 申请借阅资格
Qualification qualification = new Qualification();
qualification.setStudentId(studentBO.getId());
qualification.setStatus(ExamineEnum.TO_BE_EXAMINE.getCode());
qualification.setGmtCreate(new Date());
qualification.setGmtModified(new Date());
qualificationMapper.insertSelective(qualification);
return null;
});
}
二、web层 StudentController
service层实现以后,我们通过创建StudentController对外提供restful API,因为主要是新增,我们使用POST
请求,@RequestBody
方式,定义StudentVO
如下:
@Data
public class StudentVO implements Serializable {
@NotBlank(message = "学号不能为空")
private String studentNo;
@NotBlank(message = "学生姓名不能为空")
private String studentName;
@NotBlank(message = "学生昵称不能为空")
private String nickName;
@NotBlank(message = "学生所属院系不能为空")
private String department;
@NotBlank(message = "学生证照片不能为空")
private String idCardImage;
}
这里使用了前面讲过的Spring Validation
constraints
中的@NotBlank
注解,这个注解适用于String类型,加了以后不能为null,并且trim()以后 size>0。
定义API:apply代码如下:
@PostMapping("/apply")
public TgResult<StudentBO> apply(@Valid @RequestBody StudentVO studentVO) {
Integer userId = AuthContextInfo.getAuthInfo().loginUserId();
StudentBO studentBO = CopyUtils.copy(studentVO, StudentBO::new);
studentBO.setUserId(userId);
studentService.apply(studentBO);
return TgResult.ok(studentBO);
}
三、测试
新注册一个学生账号li2gou
,userId = 3。
调用上文的查询学生信息API,返回为空。
调用本文实现的apply
API,结果如下:
看一下数据库student表
:
看一下数据库qualification表
:
一切如期望一样,再试一下重复提交
:
最后再试一下insert student
成功,insert qualification
时抛出异常,看看是否会回滚student,我测试时是将verify_user_id
在数据库表临时设置为不允许为空,最后成功回滚
~
OK,一切如期望一样,收工!
最后
看到这,觉得有帮助的,刷波666,感谢大家的支持~
想要看更多实战好文章,还是给大家推荐我的实战专栏–>《基于SpringBoot+SpringCloud+Vue前后端分离项目实战》,由我和 前端狗哥 合力打造的一款专栏,可以让你从0到1快速拥有企业级规范的项目实战经验!
具体的优势、规划、技术选型都可以在《开篇》试读!
订阅专栏后可以添加我的微信,我会为每一位用户进行针对性指导!
另外,别忘了关注我:天罡gg ,怕你找不到我,发布新文不容易错过: https://blog.csdn.net/scm_2008