How to Implement a Soft Delete with Spring JPA – 如何用Spring JPA实现软删除

最后修改: 2021年 5月 19日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

Physically deleting data from a table is a common requirement when interacting with databases. But sometimes there are business requirements to not permanently delete data from the database. These requirements, for example, the need for data history tracking or audit and also related to reference integrity.

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

Instead of physically deleting the data, we can just hide that data so that it can’t be accessed from the application front-end.

我们可以不从物理上删除数据,而只是隐藏这些数据,使其不能从应用程序前端访问。

In this tutorial, we’ll learn about soft delete and how to implement this technique with Spring JPA.

在本教程中,我们将学习软删除以及如何使用Spring JPA实现这一技术。

2. What Is Soft Delete?

2.什么是软删除?

Soft delete performs an update process to mark some data as deleted instead of physically deleting it from a table in the database. A common way to implement soft delete is to add a field that will indicate whether data has been deleted or not.

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

For example, let’s suppose we have a product table with the following structure:

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

Let’s now look at the SQL command we’ll run when physically deleting a record from the table:

现在让我们来看看当从表中实际删除一条记录时,我们将运行的SQL命令。

delete from table_product where id=1

This SQL command will permanently remove the product with id=1 from the table in the database.

这条SQL命令将永久地从数据库的表中删除带有id=1的产品。

Let’s now implement the soft delete mechanism described above:

现在让我们来实现上述的软删除机制。

Note we added a new field called deleted. This field will contain the values 0 or 1.

注意我们添加了一个新的字段,叫做deleted。这个字段将包含01的值。

The value 1 will indicate the data has been deleted and 0 will indicate the data has not been deleted. We should set 0 as the default value, and for every data deletion process, we don’t run the SQL delete command, but the following SQL update command instead:

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

update from table_product set deleted=1 where id=1

Using this SQL command we didn’t actually delete the row, but only marked it as deleted. So, when we’re going to perform a read query, and we only want those rows that have not been deleted, we should only add a filter in our SQL query:

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

select * from table_product where deleted=0

3. How to Implement Soft Delete in Spring JPA

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

With Spring JPA the implementation of soft delete has become much easier. We’ll only need a few JPA annotations for this purpose.

有了Spring JPA,软删除的实现变得更加容易。我们只需要一些JPA注解就可以达到这个目的。

As we know, we generally use only a few SQL commands with JPA. It will create and execute the majority of the SQL queries behind the scenes.

正如我们所知,我们一般只使用JPA的几个SQL命令。它将在幕后创建并执行大部分的SQL查询。

Let’s now implement the soft delete in Spring JPA with the same table example as above.

现在让我们在Spring JPA中以上述相同的表为例实现软删除。

3.1. Entity Class

3.1.实体类

The most important part is creating the entity class.

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

Let’s create a Product entity class:

让我们创建一个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
}

As we can see, we’ve added a deleted property with the default value set as FALSE.

我们可以看到,我们已经添加了一个deleted属性,其默认值设置为FALSE

The next step will be to override the delete command in the JPA repository.

下一步将是覆盖JPA资源库中的delete命令。

By default, the delete command in the JPA repository will run a SQL delete query, so let’s first add some annotations to our entity class:

默认情况下,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
}

We are using the @SQLDelete annotation to override the delete command. Every time we execute the delete command, we actually have turned it into a SQL update command that changes the deleted field value to true instead of deleting the data permanently.

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

The @Where annotation, on the other hand, will add a filter when we read the product data. So, according to the code example above, product data with the value deleted = true won’t be included within the results.

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

3.2. Repository

3.2.存储库

There are no special changes in the repository class, we can write it like a normal repository class in the Spring Boot application:

仓库类没有什么特别的变化,我们可以像Spring Boot应用程序中的普通仓库类一样编写它。

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

3.3. Service

3.3.服务

Also for the service class, there is nothing special yet. We can call the functions from the repository that we want.

同样对于服务类,还没有什么特别的东西。我们可以从资源库中调用我们想要的函数。

In this example, let’s call three repository functions to create a record, and then perform a soft delete:

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

@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. How to Get the Deleted Data?

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

By using the @Where annotation, we can’t get the deleted product data in case we still want the deleted data to be accessible. An example of this is a user with administrator-level that has full access and can view the data that has been “deleted”.

通过使用@Where注解,我们无法获得被删除的产品数据,以防我们仍然希望被删除的数据能够被访问。这方面的一个例子是,一个具有管理员级别的用户拥有完全的访问权,可以查看已经 “删除 “的数据。

To implement this, we shouldn’t use the @Where annotation but two different annotations, @FilterDef, and @Filter. With these annotations we can dynamically add conditions as needed:

为了实现这一点,我们不应该使用@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;
}

Here @FilterDef annotation defines the basic requirements that will be used by @Filter annotation. Furthermore, we also need to change the findAll() function in the ProductService service class to handle dynamic parameters or filters:

这里@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;
    }
}

Here we add the isDeleted parameter that we’ll add to the object Filter affecting the process of reading the Product entity.

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

5. Conclusion

5.总结

It’s easy to implement soft delete techniques using Spring JPA. What we need to do is define a field that will store whether a row has been deleted or not. Then we’ve to override the delete command using the @SQLDelete annotation on that particular entity class.

使用Spring JPA实现软删除技术很容易。我们需要做的是定义一个字段来存储某行是否被删除。然后,我们要使用@SQLDelete注解来覆盖该实体类的删除命令。

If we want more control, we can use the @FilterDef and @Filter annotations so we can determine if query results should include deleted data or not.

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

All the code in this article is available over on GitHub.

本文中的所有代码都可以在GitHub上找到