创新实训个人进度博客(13)——JPA事务简析
在写学生创建创新实训的接口时,需要一步进行两个sql存放操作。首先是根据学生传来的项目申请的有关信息,创建项目实体并存入表
project
中,因为项目ID是数据库中设置的自增的所以必须先存进去然后根据申请人查出来项目ID,再结合学生信息存入student_project
表中。这两个存放步骤必须按顺序进行,且必须同时成功或同时失败。即如果第一步成功了,但是第二步失败了,那么就要回滚第一步的操作,这几需要用到数据库中一个很重要的知识:事务。
什么是数据库事务
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务的特性
1、原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
2、一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。
3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
通俗易懂的例子
转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:
1.将A账户的金额减少100元
2.将B账户的金额增加100元。
在这个过程中可能会出现以下问题:
1.转账操作的第一步执行成功,A账户上的钱减少了100元,但是第二步执行失败或者未执行便发生系统崩溃,导致B账户并没有相应增加100元。
2.转账操作刚完成就发生系统崩溃,系统重启恢复时丢失了崩溃前的转账记录。
3.同时又另一个用户转账给B账户,由于同时对B账户进行操作,导致B账户金额出现异常。
如果不能保证这两步操作要么全部执行,要么全部不执行,那么就会出现上述异常。
SpringBoot中实现事务的方法
这里我采用的是注解的方式,即@Transactional
代码:
@Transactional
public class ProjectServiceImpl implements ProjectService {
/**
* 学生申请项目
*
* @param project
* @param request
* @param response
* @return
*/
@Override
public ResponseResult StudentaddProject(Project project, HttpServletRequest request, HttpServletResponse response) {
//校验学生登录
Student student = studentService.checkStudent(request,response);
if(student == null){
return ResponseResult.ACCOUNT_NOT_LOGIN();
}
if(student.getStudentId() == null){
return ResponseResult.ACCOUNT_NOT_LOGIN();
}
log.info("学生 "+student.getStudentId()+" 正在申请创新实训 "+project.getProjectName());
//检查学生是否已经存在于一个项目中
StudentProjectGrade spg = studentProjectGradeDao.findByStudentId(student.getStudentId());
if(spg != null){
log.info("申请人已经存在于一个项目中,不可再次申请");
return ResponseResult.FAILED("申请人已经存在于一个项目中,不可再次申请!");
}
//不可以指定ID
log.info(""+project.getProjectId());
if(project.getProjectId() != 0){
return ResponseResult.FAILED("不可以指定ID");
}
//检查实训ID是否是学生所属的实训
int trainingIdFromProject = project.getTrainingId();
Student studentById = studentDao.findStudentByStudentId(student.getStudentId());
if(trainingIdFromProject != studentById.getTrainingId()){
log.info("实训ID错误");
return ResponseResult.FAILED("操作有误!请检查实训ID");
}
log.info("校验通过");
//设置默认值
project.setProjectApplicantType("学生"+student.getStudentId());
project.setProjectStatus(Constants.Status.STATUS5); //设置状态为等待教师审核
project.setProjectMaxNum(6);
project.setProjectGroupNum(1);
log.info("正在添加");
projectDao.save(project);
log.info("添加成功");
//把学生添加到小组列表
//获取刚刚存入的项目的项目ID
int projectId = projectDao.findByProjectApplicantType("学生"+student.getStudentId()).getProjectId();
log.info("已经添加到了项目表,项目ID为 == > "+projectId);
//构建实体类
StudentProjectGrade spgtoSave = new StudentProjectGrade();
spgtoSave.setStudentId(studentById.getStudentId());
spgtoSave.setProjectId(projectId + 100);
spgtoSave.setGrades5Points(-1);
spgtoSave.setGrades100Points(-1);
spgtoSave.setIsjointed(1);
spgtoSave.setTrainingId(studentById.getTrainingId());
spgtoSave.setIsleader(1);
//存入相应的表
studentProjectGradeDao.save(spgtoSave);
log.info("已经添加到了学生_项目表");
log.info("");
//返回结果
return ResponseResult.SUCCESS("创建项目成功");
}
}
两处save操作必须保持事务的完整性,如果第一步执行了而第二步没有执行,那么就会造成这个学生创建的项目无人参与的情况。这里我故意更改了第二步save时的主键ID,让他不满足数据库中的外键依赖,然后运行,log如下
如果我们仅从log来判断,那么第一步的确是完成了,而第二步违反外键依赖,报错了。而且log还正确输出了自动生成的项目ID。然而在检查数据库时发现,并没有编号为29的项目。说明这一步被数据库自动回滚了。因此证明注解方法可以保持事务的完整性。
当把代码中的id+100去掉后,再次运行
输出正常,但是生成的ID是30号,也许是事务实现时先把这一行创建了出来,然后回滚时再删掉的。