写点什么

数据的软删除—什么时候需要?又如何去实现?

作者:翊君
  • 2022 年 6 月 10 日
  • 本文字数:3810 字

    阅读完需:约 13 分钟

数据的软删除—什么时候需要?又如何去实现?

0. 阅读完本文你将会学会

  • 什么是软删除?

  • 如何考量是否使用软删除

  • 如何在 Spring 里实现软删除

1. 前言

我们在开发程序的过程中,会遇到一个常见的需求——删除表中的数据。


但是有时候,业务需求要求不能永久删除数据库中的数据。比如一些敏感信息,我们需要留着以方便做历史追踪。


这个时候,我们便会用到软删除。


那么什么是软删除?什么时候才能使用它?在本文中,笔者将会带你学习软删除以及如何用 Spring Data JPA 实现它。

2. 什么是软删除(Soft Delete)?

2.1 软删除的概念

软删除(Soft Delete)是相对于硬删除(Hard Delete)来说的,它又可以叫做逻辑删除或者标记删除。


这种删除方式并不是真正地从数据库中把记录删除,而是通过特定的标记方式在查询的时候将此记录过滤掉。虽然数据在界面上已经看不见,但是数据库还是存在的。

2.2 软删除的实现方式

  1. 添加布尔类型的字段


添加类似于is_deleted或者is_active或者is_archived的布尔型字段,以此来标记是否删除。


  1. 添加时间戳字段


添加类似于deleted_at的时间戳字段,null 表示未删除,非 null 则表示已经删除,也能获取删除的时间。


  1. 将软删除的数据插入到另一个表中。


举个例子,order表会有一个相应的order_deleted表,在删除order表中的数据,将数据复制到order_deleted表中。


在以上三种方式中,第 1 种方式算是最普遍的,也较为简单;


第 2 种方式虽然对于第 1 种方式会更加严谨一点,因为它可以获取准确的删除时间。但是第 2 种方式在查询的性能方面却是比较差的,因为 null 值会导致全表扫描,导致查询效率大打折扣。


我们可以混用第 1 种和第 2 种方式,只用第 1 种方式来做条件,再用第 2 种方式的删除时间做补充。


第 3 种方式,思路与前两种方式完全不同,当数据量大的时候,我们可以考虑采用这一策略。

2.3 是否采用软删除的考量

其实在业务逻辑中采用“删除”这个词是不准确的。


比如说,我们“删除”某种产品的时候其实是指我们“停售”了。可能以后不会再卖这种产品了,顾客搜索也不会看见这种商品,但是管理仓库的人暂时还需要管理它的库存。


所以,“删除”是不准确的说法,只是为了图方便。


按照Udi Dahan的解读来看:


  • 订单不是被删除的,而是被“取消”的,订单取消得太晚,还会产生花费;

  • 员工不是被删除的,而是被“解雇”的或者“退休”的。还有相应的补偿金要处理;

  • 职位不是被删除的,是被“填补”的(或者招聘申请被驳回)。


真实的世界并不是级联的


假设市场部要从商品目录中删除一样商品,那是不是说所有包含了该商品的旧订单都要一并消失?再级联下去,这些订单对应的所有发票也要删除吗?就这么一步步删下去,是不是公司的损益报表也要重做了?


这样看起来明显不合理吧。


那我们在实际的业务逻辑中是否采用软删除?


软删除的好处显而易见,它是一味后悔药,利于历史追踪或者为了审计目的(History tracking or audit)。


当然软删除也有弊端,不利于数据库性能(主要针对关系型数据库)的提升,可能会产生大量的冗余数据。


如果我们不需要,请不要画蛇添足,当我们需要的时候,请考虑业务的数据量和读写方式。


当需要软删除的时候,我们设置一个状态字段,用来表示数据是否还有效。当然,我们也可以采用一个拥有多重状态的字段:有效、停用、取消、弃置等等。我们可以借助这样一个状态字段来回溯过去的字段,以此进行分析。

3. 在 Spring 中实现软删除

在 Spring Data JPA 的帮助下,实现软删除变得非常简单。我们只需要添加一些注释即可。


现在让我们来看看如何实现这一功能:

3.1 实体类 Product

清单 3.1.1 实体类 Product


package com.jayxu.mydemo.persistence.entity;
import javax.persistence.Entity;import javax.persistence.Table;
@Entity@Table(name = "product")public class Product {
private long id;
private String name;
private double price;
private String description;
private boolean isDeleted = Boolean.FALSE;
// getter setter methods}
复制代码


在上面这段代码中,我们添加了一个布尔类型的属性——isDeleted用来标记是否已删除。


下一步,我们重写 JPA 的 delete 命令。


一般来说,JPA 的 delete 命令将会运行一条 delete 的 SQL,所以我们先在上面的实体类上增加一些注解:


清单 3.1.2 增加了注解后的实体类 Product


@Entity@Table(name = "product")@SQLDelete(sql = "UPDATE product SET is_deleted = true WHERE id = ?")@Where(clause = "is_deleted = false")public class Product {  // . . .}
复制代码


@SQLDelete注释用来覆盖 delete 命令,每次我们执行 delete 命令时,我们会将其转化成清单 3.1.2 中的 UPDATE 语句,这条命令将isDeleted字段更改为 true,而不是永久删除数据。


除此之外,@where注释将会提供一个过滤器,当我们需要读取Product数据时,结果中不会包含is_deleted = true的数据。

3.2 Repository

Repository 类没有任何特殊变化:


清单 3.2.1 ProductRepository


package com.jayxu.mydemo.repository;
import com.jayxu.mydemo.persistence.entity.Product;import org.springframework.data.repository.CrudRepository;
public interface ProductRepository extends CrudRepository<Product, Long> {}
复制代码

3.3 Service

对于 Service 来说,也没有什么特别之处。


在下面这个例子中,我们创建了一条记录,执行了一个软删除,查找出所有的实体类。


清单 3.3.1 ProductService


package com.jayxu.mydemo.service;
import com.jayxu.mydemo.persistence.entity.Product;import com.jayxu.mydemo.repository.ProductRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
@Servicepublic class ProductService {
@Autowired ProductRepository productRepository;
public Product create(Product product){ return productRepository.save(product); }
public void deleteById(long id){ productRepository.deleteById(id); } public Iterable<Product> findAll(){ return productRepository.findAll(); }}
复制代码

3.4 如何获取已被删除的数据

上文中提到,对于软删除的数据,我们会用作历史追踪或者出于其他的目的。那么如何获取已经被软删除的数据呢?


使用了@Where注释,我们得不到这些数据,我们可以考虑使用@FilterDef@Filter注解。通过使用这些注解,我们可以根据需求动态添加查询条件。


清单 3.4.1 实体类 Product


package com.jayxu.mydemo.persistence.entity;
import org.hibernate.annotations.*;
import javax.persistence.Entity;import javax.persistence.Table;
@Entity@Table(name = "product")@SQLDelete(sql = "UPDATE product SET is_deleted = true WHERE id = ?")@FilterDef(name = "removedProductFilter" , parameters = @ParamDef(name = "isDeleted", type = "boolean"))@Filter(name = "removedProductFilter", condition = "is_deleted = :isDeleted")public class Product { // . . .}
复制代码


在上面这段代码中,@FilterDef 定义了@Filter注解所需要的参数。@Filter一般用来定义在实体类上。


除了这个改动之外,我们还需要改写下ProductService中的findAll() 方法。


清单 3.4.2 改动后的 ProductService


package com.jayxu.mydemo.service;
import com.jayxu.mydemo.persistence.entity.Product;import com.jayxu.mydemo.repository.ProductRepository;import org.hibernate.Filter;import org.hibernate.Session;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
@Servicepublic class ProductService {
@Autowired ProductRepository productRepository;
@Autowired EntityManager entityManager;
private String FILTER_REMOVED_PRODUCT = "removedProductFilter";
private String PARAM_IS_DELETED = "isDeleted";
public Product create(Product product){ return productRepository.save(product); }
public void deleteById(long id){ productRepository.deleteById(id); }
public Iterable<Product> findAll(boolean isDeleted){ Session session = entityManager.unwrap(Session.class); Filter removedProductFilter = session.enableFilter(FILTER_REMOVED_PRODUCT); removedProductFilter.setParameter(PARAM_IS_DELETED, isDeleted);
Iterable<Product> products = productRepository.findAll(); session.disableFilter(FILTER_REMOVED_PRODUCT); return products; }}
复制代码


在清单 3.4.2 中,我们先是通过session.enableFilter()激活定义的removedProductFilter,再将传入的参数设置进去,然后查询完毕,最后通过session.disableFilter()关闭removedProductFilter


当然除了这种方式之外,我们还可以直接在ProductRepository中写findAllByIsDeleted()方法,这种方式更加简洁,可以自己尝试一下。

4. 结语

相信看到这里,你对软删除的概念、是否需要软删除的考量以及怎么使用 Spring Data JPA 实现软删除有了一定的了解,那么现在打开电脑,自己尝试下这个小功能吧!


如果看到这里还觉得不过瘾,可以看看我的往期精选哦!


发布于: 刚刚阅读数: 5
用户头像

翊君

关注

周而不比 和而不同 2019.02.11 加入

喜欢阅读、摄影、写文的一枚小码农

评论

发布
暂无评论
数据的软删除—什么时候需要?又如何去实现?_6月日更_翊君_InfoQ写作社区