spring boot实现软删除的示例代码

时间:2021-11-19 00:51:43

本文开发环境:spring-boot:2.0.3.release + java1.8

why to do

软删除:即不进行真正的删除操作。由于我们实体间的约束性(外键)的存在,删除某些数据后,将导致其它的数据不完整。比如,计算机1801班的教师是张三,此时,我们如果把张三删除掉,那么在查询计算机1801班时,由于张三不存了,所以就会报entitynotfound的错误。当然了,在有外键约束的数据库中,如果张三是1801班的教师,那么我们直接删除张三将报一个约束性的异常。也就是说:直接删除张三这个行为是无法执行的。

但有些时候,我们的确有删除的需求。比如说,有个员工离职了,然后我们想在员工管理中删除该员工。但是:该员工由于在数据表中存在历史记录。比如我们记录了17年第二学期的数据结构是张三教的。那么,由于约束性的存在,删除张三时就会报约束性错误。也就是说:出现了应该删除,但却删除不了的尴尬。

这就用到了本文所提到的软删除,所谓软删除,就是说我并不真正的删除数据表中的数据,而是在给这条记录加一个是否删除的标记。

spring jpa是支持软删除的,我们可以找到较多质量不错的文章来解决这个问题。大体步骤为:1. 加入@sqldelete("update xxxx set deleted = 1 where id = ?")。2.加入@where(clause = "deleted = false")的注解。但这个解决方案并不完美。具体表现在:

我们还以张三是1801班的教师举例。

加入注解后,我们的确是可以做到可以成功的删除张三了,删除操作后,我们查看数据表,张三的记录的确也还在。但此时,如果我们进行all或是page查询,将得到一个500 entiynotfound错误。这是由于在all查询时,jpa自动加入了@where中的的查询参数,由于关联数据的deleted = true,进而发生了未找到关联实体的异常。

但事实是:实体虽然被删除,但实际在还在,我们想将其应用到关联查询中。并不希望其发生500 entiynotfound异常。

本文的方案实现了:

  1. 可以成功的实现软删除。
  2. 再进行关联删除时,不发生500 entiynotfound错误。

解决方案

  1. 即然500是由于注解@where(clause = "deleted = false")引起的,那么我们弃用该注解。
  2. 我们需要在查询时,加入deleted = false的查询。那么我们新建一个接口,并继承jpa的crudrepository,然后重写其查询相关的方法。在重写过程中,加入deleted = false的查询条件。

实施

spring boot实现软删除的示例代码

初始化

新建clazztest, clazz, teacher三个实体,新建baseentity抽象类实体。其中clazztest用于演示使用@where(clause = "deleted = false")注解时发生的异常。

?
1
2
3
4
5
6
7
8
9
package com.mengyunzhi.springbootsamplecode.softdelete.entity;
 
import javax.persistence.mappedsuperclass;
 
@mappedsuperclass
public abstract class baseentity {
  private boolean deleted = false;
  // setter and getter
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.mengyunzhi.springbootsamplecode.softdelete.entity;
 
import org.hibernate.annotations.sqldelete;
 
import javax.persistence.entity;
import javax.persistence.generatedvalue;
import javax.persistence.id;
 
/**
 * 班级
 */
@entity
@sqldelete(sql = "update `klass` set deleted = 1 where id = ?")
public class klass extends baseentity {
  @id
  @generatedvalue
  private long id;
  private string name;
  // setter and getter
}
?
1
2
3
4
5
6
7
8
@entity
@sqldelete(sql = "update `klass_test` set deleted = 1 where id = ?")
@where(clause = "deleted = false")
public class klasstest extends baseentity {
  @id @generatedvalue
  private long id;
  private string name;
}

重写crudrepository

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.mengyunzhi.springbootsamplecode.softdelete.core;
 
 
import org.springframework.data.jpa.repository.query;
import org.springframework.data.repository.crudrepository;
import org.springframework.data.repository.norepositorybean;
 
import javax.transaction.transactional;
import java.util.optional;
 
/**
 * 应用软删除
 * 默认的@where(clause = "deleted = 0")会导致hibernate内部进行关联查询时,发生objectnotfound的异常
 * 在此重新定义接口
 * 参考:https://*.com/questions/19323557/handling-soft-deletes-with-spring-jpa/22202469
 * @author 河北工业大学 梦云智软件开发团队
 */
@norepositorybean
public interface softdeletecrudrepository<t, id> extends crudrepository<t, id> {
  @override
  @transactional
  @query("select e from #{#entityname} e where e.id = ?1 and e.deleted = false")
  optional<t> findbyid(id id);
 
  @override
  @transactional
  default boolean existsbyid(id id) {
    return findbyid(id).ispresent();
  }
 
  @override
  @transactional
  @query("select e from #{#entityname} e where e.deleted = false")
  iterable<t> findall();
 
  @override
  @transactional
  @query("select e from #{#entityname} e where e.id in ?1 and e.deleted = false")
  iterable<t> findallbyid(iterable<id> ids);
 
  @override
  @transactional
  @query("select count(e) from #{#entityname} e where e.deleted = false")
  long count();
}

新建仓库类

继承spring的crudrepository。

?
1
2
3
4
5
6
/**
 * 班级
 * @author panjie
 */
public interface klassrepository extends softdeletecrudrepository<klass, long>{
}
?
1
2
public interface klasstestrepository extends softdeletecrudrepository<klasstest, long> {
}
?
1
2
public interface teacherrepository extends crudrepository<teacher, long> {
}

测试

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.mengyunzhi.springbootsamplecode.softdelete.repository;
 
import com.mengyunzhi.springbootsamplecode.softdelete.entity.klass;
import com.mengyunzhi.springbootsamplecode.softdelete.entity.klasstest;
import com.mengyunzhi.springbootsamplecode.softdelete.entity.teacher;
import org.assertj.core.api.assertions;
import org.junit.test;
import org.junit.runner.runwith;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.boot.test.context.springboottest;
import org.springframework.orm.jpa.jpaobjectretrievalfailureexception;
import org.springframework.test.context.junit4.springrunner;
 
import java.util.list;
import java.util.optional;
 
 
/**
 * @author panjie
 */
@springboottest
@runwith(springrunner.class)
public class teacherrepositorytest {
  private final static logger logger = loggerfactory.getlogger(teacherrepositorytest.class);
  @autowired klassrepository klassrepository;
  @autowired klasstestrepository klasstestrepository;
  @autowired teacherrepository teacherrepository;
 
  @test
  public void findbyid() {
    logger.info("新建一个有klass和klasstest的教师");
    klass klass = new klass();
    klassrepository.save(klass);
    klasstest klasstest = new klasstest();
    klasstestrepository.save(klasstest);
    teacher teacher = new teacher();
    teacher.setklass(klass);
    teacher.setklasstest(klasstest);
    teacherrepository.save(teacher);
 
    logger.info("查找教师,断言查找了实体,并且不发生异常");
    optional<teacher> teacheroptional = teacherrepository.findbyid(teacher.getid());
    assertions.assertthat(teacheroptional.get()).isnotnull();
 
 
    logger.info("删除关联的klass, 再查找教师实体,断言查找到了实体,不发生异常。断言教师实体中,仍然存在已经删除的klass实体");
    klassrepository.deletebyid(klass.getid());
    teacheroptional = teacherrepository.findbyid(teacher.getid());
    assertions.assertthat(teacheroptional.get()).isnotnull();
    assertions.assertthat(teacheroptional.get().getklass().getid()).isequalto(klass.getid());
 
    logger.info("查找教师列表,不发生异常。断言教师实体中,存在已删除的klass实体记录");
    list<teacher> teacherlist = (list<teacher>) teacherrepository.findall();
    for (teacher teacher1 : teacherlist) {
      assertions.assertthat(teacher1.getklass().getid()).isequalto(klass.getid());
    }
 
    logger.info("删除关联的klasstest,再查找教师实体, 断言找到了删除的klasstest");
    klasstestrepository.deletebyid(klasstest.getid());
    teacheroptional = teacherrepository.findbyid(teacher.getid());
    assertions.assertthat(teacheroptional.get()).isnotnull();
    assertions.assertthat(teacheroptional.get().getklasstest().getid()).isequalto(klasstest.getid());
 
    logger.info("再查找教师列表,断言将发生jpaobjectretrievalfailureexception(entitynotfound 异常被捕获后,封装抛出)异常");
    boolean catchexception = false;
    try {
      teacherrepository.findall();
    } catch (jpaobjectretrievalfailureexception e) {
      catchexception = true;
    }
    assertions.assertthat(catchexception).istrue();
  }
 
}

总结

使用默认的@sqldelete以及@where注解时,jpa data能够很好的处理findbyid()方法,但却未能很好的处理findall()方法。在此,我们通过重写crunrepository的方法,实现了,将进行基本的查询时,使用我们自定义的加入了deleted = true的方法。而当jpa进行关联查询时,由于我们未设置@where注解,所以将查询出所有的数据,进而避免了当进行findall()查询时,有被删除的关联数据时而发生的异常。

本文中,我们只给了部分示例代码。

如果你需要完整的代码,请点击:https://github.com/mengyunzhi/springbootsamplecode/tree/master/softdelete

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://segmentfault.com/a/1190000015459505