2

零、前言

为了方便描述,我们将项目进行一下抽象和简化。
这是一个前端用Angular、后端用Spring的项目,项目E-R图的其中一段如下:
image.png
不难看出,乡镇和社区是1对多的关系。

在管理较低级的区域时,需要关联到较高级区域的外键(例如:社区必须有一个所属的乡镇)

image.png

由于这几种区域的查询都很频繁,为减少SQL频率,在后台设置了缓存。
为了避免删除数据导致整个系统的错误,全局启用了软删除。

一、问题复现

在任何一个实体的管理页面(如乡镇管理)中删除一个对象(乡镇),列表中不再显示删除的数据,数据库中也能看到,已删除对象的deleted=1:

image.png
image.png
但是在其他实体关联查询时(社区中设置乡镇时),确可以查到已经删除的对象,而且居然还能保存...

image.png

(如果保存已删除的数据,会导致系统报错)

简单总结一下就是:

由于项目代码的某些问题,软删除在列表分页查询时正常,但到了需要外键关联时,软删除却失效了。

二、排查问题

排除缓存原因

首先从issue上看,可能是后端的缓存导致的(之前出现过类似的问题)。后端设置了重新登录时清除缓存,因此测试很简单。
尝试了浏览器刷新、退出重新登录、换浏览器等操作,并没有解决问题,现在基本上排除缓存原因了。

进一步排查,Spring中使用debug模式步进查询功能的内部代码,发现返回值中出现了被删除的信息

image.png

至此可以断定不是缓存问题而是查询方法的问题。

检查调用关系

既然是查询出了问题,为什么在乡镇列表却可以正常区分已删除的数据呢?

带着疑问,我找到了前后端的调用关系:

乡镇列表发起的分页查询,最终调用到findAll方法

image.png

而在社区->乡镇选择器中查询乡镇,最终也会调用到findAll方法,但参数不同
image.png

// findAll没有参数
@Override
public List<Town> findAll() {
  return (List<Town>) this.townRepository.findAll();
}

// page有参数
@Override
public Page<Town> page(String name, Pageable pageable) {
  return this.townRepository.findAll(TownSpecs.containingName(name)), pageable);
}

于是初步判断,是仓库层TownRepository的getAll()方法漏写了软删除相关的功能导致的,但目前我们看到的代码中,并没有关于软删除是如何实现的,所以继续找。

探索软删除的实现

既然已经知道问题出在哪,接下来就去找,在这个项目中,软删除是怎么实现的,以及影响仓库层查询的关键的代码在哪里,我从历史的Pull Request中找到软删除的PR。

发现本项目中,所有的实体都继承了基础实体,启用软删除需要在基础实体中设置deleted和deleteAt字段,以及相关的Setter、Getter方法,用来表示已删除和删除时间:
image.png

然后在所有的继承类上添加@SQLDelete注解,把删除功能替换成”设置deleted=1“
第二行@where(clause = "deleted = false") 作用是在查询时只查询没有被软删除的数据。
image.png

此时我想到了一个笨方法:在仓库层所有的findAll上增加deleted = 0 的条件,但问题是,这么多的实体,会产生大量重复代码,而且也没有从根本上解决问题,因此放弃。

至此,找了一圈还是没找到答案:按理说这样已经可以生效了,但为什么findAll()会不正常呢?

又比对了一下本地最新版本的代码,发现继承实体中已经删去了@where(clause = "deleted = false")
image.png

正当我纳闷的时候,发现代码注释里有一个思否链接,打开一看正是潘老师之前写的软删除的博客,于是我又读了一遍。
spring boot实现软删除

这才了解到:
@Where(clause = "deleted = false")会导致我们在进行all或page查询时,得到一个500 EntiyNotFound错误。

博客中也给出了解决方法:创建一个SoftDeleteCrudRepository接口,继承并覆盖JPA内部的CrudRepository的方法,手动的为查询方法添加deleted = false条件(具体代码见博客),这样既能实现软删除又避免了500错误。

三、解开BUG的神秘面纱

所以可以猜测到,问题一定是出现在我们自己写的SoftDeleteCrudRepository上,大概是因为某些方法没有override。

来到此项目的软删除仓库中,关于findAll的重载方法有这些:

  @Override
  public Page<T> findAll(Pageable pageable) {
    return this.findAll(this.andDeleteFalseSpecification(null), pageable);
  }

  @Override
  public Page<T> findAll(@Nullable Specification<T> specification, Pageable pageable) {
    return super.findAll(this.andDeleteFalseSpecification(specification), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification, Sort sort) {
    return super.findAll(this.andDeleteFalseSpecification(specification), sort);
  }

我们再来回顾一下,刚才的两种情况是怎么调用的:

// findAll没有参数
@Override
public List<Town> findAll() {
  return (List<Town>) this.townRepository.findAll();
}

// page有参数
@Override
public Page<Town> page(String name, Pageable pageable) {
  return this.townRepository.findAll(TownSpecs.containingName(name)), pageable);
}

好,破案了,我们的软删除类中并没有override空参数情况的findAll方法,因此对于空参数,并没有自动加上deleted=1 的查询条件。

所以只需要在这里加上:

  @Override
  public List<T> findAll(@Nullable Specification<T> specification) {
    return super.findAll(this.andDeleteFalseSpecification(specification));
  }

就可以让本文的bug在所有仓库的findAll()方法中消失,日后再出现新的调用方式,也只需修改软删除类即可。
至此问题终于解决。

参考资料

后记

其实这个项目真正的 写法比参考资料中更复杂:

  • SoftDeleteCrudRepository不再是接口,而是实现类
  • 其他仓库并不是直接继承SoftDeleteCrudRepository,而是使用工厂模式注入
  • 项目中工厂模式的代码我没看懂,其实最后也没搞明白,其他仓库在没有extends也没有implements的情况下,是怎么调用SoftDeleteCrudRepository的。

版权声明

本文作者:河北工业大学梦云智开发团队 - 刘宇轩
新人经验不足,有建议欢迎交流,有错误欢迎轻喷

LYX6666
1.6k 声望75 粉丝

一个正在茁壮成长的零基础小白