一、简介

从表中物理删除数据是与数据库交互时的常见要求。但有时业务要求不从数据库中永久删除数据。这些要求,例如,需要数据历史跟踪或审计,也与参考完整性相关。

我们可以隐藏该数据,以便无法从应用程序前端访问它,而不是物理删除数据。

在本教程中,我们将了解软删除以及如何使用Spring JPA实现此技术。

2. 什么是软删除?

软删除执行更新过程以将某些数据标记为已删除,而不是从数据库的表中物理删除它。 实现软删除的常见方法是添加一个字段来指示数据是否已被删除。

例如,假设我们有一个具有以下结构的产品表: 表格1

现在让我们看一下从表中物理删除记录时将运行的 SQL 命令:

delete from table_product where id=1

此 SQL 命令将从数据库的表中永久删除 id=1 的产品。

现在让我们实现上面描述的软删除机制: 表2

请注意,我们添加了一个名为 “已删除”的新字段。 该字段将包含值 01

值为 1 表示数据已被删除,值为 0 表示数据尚未删除。我们应该将 0 设置为默认值,并且对于每个数据删除过程,我们不运行SQL删除命令,而是运行以下SQL更新命令:

update from table_product set deleted=1 where id=1

使用此 SQL 命令,我们实际上并没有删除该行,而只是将其标记为已删除。因此,当我们要执行读取查询时,并且只想要那些尚未删除的行,我们应该只在 SQL 查询中添加一个过滤器:

select * from table_product where deleted=0

3. Spring JPA中如何实现软删除

使用Spring JPA,软删除的实现变得更加容易。为此,我们只需要一些 JPA 注释。

众所周知,我们通常只在 JPA 中使用很少的 SQL 命令。它将在幕后创建并执行大部分 SQL 查询。

现在让我们使用与上面相同的表示例在 Spring JPA 中实现软删除。

3.1.实体类

最重要的部分是创建实体类。

让我们创建一个 Product 实体类:

@Entity
@Table(name = "table_product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    private boolean deleted = Boolean.FALSE;

    // setter getter methods
}

正如我们所看到的,我们添加了一个 已删除的 属性,其默认值设置为 FALSE

下一步将覆盖 JPA 存储库中的 删除 命令。

默认情况下,JPA 存储库中的删除命令将运行 SQL 删除查询,因此我们首先向实体类添加一些注释:

@Entity
@Table(name = "table_product")
@SQLDelete(sql = "UPDATE table_product SET deleted = true WHERE id=?")
@Where(clause = "deleted=false")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    private boolean deleted = Boolean.FALSE;
   
    // setter getter method
}

我们使用 @SQLDelete 注释来覆盖删除命令。 每次执行删除命令时,我们实际上已经 将其变成了SQL更新命令,将删除的字段值更改为true, 而不是永久删除数据。

另一方面, @Where 注解会在我们读取产品数据时添加过滤器。 因此,根据上面的代码示例,值为 deleted = true的 产品数据将不会包含在结果中。

3.2.存储库

存储库类没有特殊的变化,我们可以像Spring Boot应用程序中的普通存储库类一样编写它:

public interface ProductRepository extends CrudRepository<Product, Long>{
    
}

3.3.服务

对于服务级别来说,还没有什么特别的。我们可以从存储库中调用我们想要的函数。

在这个例子中,我们调用三个存储库函数来创建一条记录,然后执行软删除:

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;

    public Product create(Product product) {
        return productRepository.save(product);
    }

    public void remove(Long id){
        productRepository.deleteById(id);
    }

    public Iterable<Product> findAll(){
        return productRepository.findAll();
    }
}

4. 如何获取已删除的数据?

通过使用 @Where 注释,如果我们仍然希望可以访问已删除的数据,则无法获取已删除的产品数据。例如,具有管理员级别的用户具有完全访问权限,可以查看已“删除”的数据。

为了实现这一点,我们不应该使用 @Where 注释 ,而应该使用两个不同的注释: @FilterDef@Filter 。通过这些注释,我们可以根据需要动态添加条件:

@Entity
@Table(name = "tbl_products")
@SQLDelete(sql = "UPDATE tbl_products SET deleted = true WHERE id=?")
@FilterDef(name = "deletedProductFilter", parameters = @ParamDef(name = "isDeleted", type = "boolean"))
@Filter(name = "deletedProductFilter", condition = "deleted = :isDeleted")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    private boolean deleted = Boolean.FALSE;
}

这里 @FilterDef 注解定义了 @Filter 注解将使用的基本要求。此外,我们还需要更改 ProductService 服务类中的 findAll() 函数来处理动态参数或过滤器:

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private EntityManager entityManager;

    public Product create(Product product) {
        return productRepository.save(product);
    }

    public void remove(Long id){
        productRepository.deleteById(id);
    }

    public Iterable<Product> findAll(boolean isDeleted){
        Session session = entityManager.unwrap(Session.class);
        Filter filter = session.enableFilter("deletedProductFilter");
        filter.setParameter("isDeleted", isDeleted);
        Iterable<Product> products =  productRepository.findAll();
        session.disableFilter("deletedProductFilter");
        return products;
    }
}

这里我们添加 isDeleted 参数,我们将添加到影响读取 Product 实体的过程的对象 Filter 中。

5. 结论

使用 Spring JPA 可以轻松实现软删除技术。我们需要做的是定义一个字段来存储行是否已被删除。然后我们必须使用该特定实体类上的 @SQLDelete 注释来覆盖删除命令。

如果我们想要更多的控制,我们可以使用 @FilterDef@Filter 注释,这样我们就可以确定查询结果是否应该包含已删除的数据。

本文中的所有代码都可以 在 GitHub 上获取。