零、前言
为了方便描述,我们将项目进行一下抽象和简化。
这是一个前端用Angular、后端用Spring的项目,项目E-R图的其中一段如下:
不难看出,乡镇和社区是1对多的关系。
在管理较低级的区域时,需要关联到较高级区域的外键(例如:社区必须有一个所属的乡镇)
由于这几种区域的查询都很频繁,为减少SQL频率,在后台设置了缓存。
为了避免删除数据导致整个系统的错误,全局启用了软删除。
一、问题复现
在任何一个实体的管理页面(如乡镇管理)中删除一个对象(乡镇),列表中不再显示删除的数据,数据库中也能看到,已删除对象的deleted=1:
但是在其他实体关联查询时(社区中设置乡镇时),确可以查到已经删除的对象,而且居然还能保存...
(如果保存已删除的数据,会导致系统报错)
简单总结一下就是:
由于项目代码的某些问题,软删除在列表分页查询时正常,但到了需要外键关联时,软删除却失效了。
二、排查问题
排除缓存原因
首先从issue上看,可能是后端的缓存导致的(之前出现过类似的问题)。后端设置了重新登录时清除缓存,因此测试很简单。
尝试了浏览器刷新、退出重新登录、换浏览器等操作,并没有解决问题,现在基本上排除缓存原因了。
进一步排查,Spring中使用debug模式步进查询功能的内部代码,发现返回值中出现了被删除的信息
至此可以断定不是缓存问题而是查询方法的问题。
检查调用关系
既然是查询出了问题,为什么在乡镇列表却可以正常区分已删除的数据呢?
带着疑问,我找到了前后端的调用关系:
乡镇列表发起的分页查询,最终调用到findAll方法
而在社区->乡镇选择器中查询乡镇,最终也会调用到findAll方法,但参数不同
// 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方法,用来表示已删除和删除时间:
然后在所有的继承类上添加@SQLDelete注解,把删除功能替换成”设置deleted=1“
第二行@where(clause = "deleted = false") 作用是在查询时只查询没有被软删除的数据。
此时我想到了一个笨方法:在仓库层所有的findAll上增加deleted = 0 的条件,但问题是,这么多的实体,会产生大量重复代码,而且也没有从根本上解决问题,因此放弃。
至此,找了一圈还是没找到答案:按理说这样已经可以生效了,但为什么findAll()会不正常呢?
又比对了一下本地最新版本的代码,发现继承实体中已经删去了@where(clause = "deleted = false")
正当我纳闷的时候,发现代码注释里有一个思否链接,打开一看正是潘老师之前写的软删除的博客,于是我又读了一遍。
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()方法中消失,日后再出现新的调用方式,也只需修改软删除类即可。
至此问题终于解决。
参考资料
- spring boot实现软删除:https://segmentfault.com/a/11...
后记
其实这个项目真正的 写法比参考资料中更复杂:
- SoftDeleteCrudRepository不再是接口,而是实现类
- 其他仓库并不是直接继承SoftDeleteCrudRepository,而是使用工厂模式注入
- 项目中工厂模式的代码我没看懂,其实最后也没搞明白,其他仓库在没有extends也没有implements的情况下,是怎么调用SoftDeleteCrudRepository的。
版权声明
本文作者:河北工业大学梦云智开发团队 - 刘宇轩
新人经验不足,有建议欢迎交流,有错误欢迎轻喷
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。