7.16 SpringBoot项目实战 【学生入驻】(下):正确理解 编程式事务和声明式事务

CSDN成就一亿技术人


前言

通过上文,我们实现了【学生入驻】的第一个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-未通过)。
修改方法:

  1. 修改generatorConfig.xmlis_approved配置:将javaType从Boolean修改为Integer,并双击generate重新生成,如下图:
    在这里插入图片描述
  2. 生成以后,请确认Student和StudentExample类已被修改。
  3. 然后,再手动修改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. 申请借阅资格】,所以:

  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

猜你喜欢

转载自blog.csdn.net/scm_2008/article/details/133253044
今日推荐