在某些应用程序中,您不希望或不允许从数据库中永久删除记录。但是,您仍然需要删除或隐藏不再处于活动状态的记录。例如,您希望保留一个用户帐户,因为它链接到仍在使用的其他业务对象。

您有 2 个基本选项可以将此信息保存在系统中。您可以保留记录所有更改的审核日志,也可以执行隐藏已删除记录的软删除。我在 关于 Hibernate Envers 的文章中 解释了审核日志选项。今天,我想向您展示如何使用 Hibernate 实现软删除。但在我这样做之前,让我快速解释一下什么是软删除。

什么是软删除?

软删除执行更新以将记录标记为已删除,而不是将其从数据库表中删除。对软删除进行建模的常见方法包括:

  • 一个布尔值,指示记录是处于活动状态还是已删除,
  • 一个枚举,用于对记录的状态进行建模, ,
  • 存储执行软删除的日期和时间的时间戳。

显然,添加这样的字段只是实现软删除功能的第一步。在保留新记录时,还必须设置它,并且必须更改其软删除指示符,而不是删除记录。若要对用户隐藏软删除记录,还必须调整所有查询,以根据记录的软删除指示器排除记录。

这听起来像是很多工作,但如果您使用的是 Hibernate,则并非如此。

如何使用 Hibernate 实现软删除

在 6.4 版本中,Hibernate 团队为 Hibernate ORM 引入了官方的软删除功能。现在只需要 1 个注释即可激活实体类的软删除。然后,Hibernate 会生成软删除记录所需的 SQL UPDATE 语句,并调整所有查询语句以排除软删除的记录。在本文的下一节中,我将向您展示如何激活软删除以及不同的配置选项。

对于 Hibernate 版本 <=6.3,您必须自行实现软删除功能。这需要一些额外的工作。但别担心,这并不复杂,您可以在实体映射中声明所有必需的部分。因此,您不必在业务代码中处理它。在 本文末尾 ,我将向您展示如何实现映射。

使用 Hibernate >= 6.4 进行软删除

Hibernate 6.4 引入了对软删除的支持。您只需要使用@SoftDelete注解来注释您的实体类 @SoftDelete ,其余的交给 Hibernate 处理。

休眠默认软删除实现

以下代码片段使用 Hibernate 的默认软删除实现。它需要 已删除 基础数据库表中一个布尔类型的 boolean列。可以使用 批注的 columnName 属性 @SoftDelete 为该列指定不同的名称。

@Entity
@SoftDelete
public class Account { ... }

时,Hibernate 会自动将 已删除 的字段设置为 false 当您保留新的 Account 实体

Account a = new Account();
a.setName("thjanssen");
em.persist(a);
10:30:49,099 DEBUG SQL:135 - insert into Account (name,deleted,id) values (?,false,?)

删除实体时,Hibernate 会将 已删除 的字段设置为 true 。Hibernate 添加了一个谓词,该谓词将 deleted=true 的所有记录排除到 所有 JPQL 和 Criteria 查询以及所有生成的语句中。

Account a = em.find(Account.class, a.getId());
em.remove(a);

TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
10:30:49,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.deleted=false and a1_0.id=?
10:30:49,211 DEBUG SQL:135 - update Account set deleted=true where id=? and deleted=false

10:30:49,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

使用不同的 SoftDeleteType

Hibernate 支持 2 种不同的 SoftDeleteType ,用于确定数据库中存储的布尔值的含义:

  1. SoftDeleteType.ACTIVE。 
    该值 true 将记录标记为活动状态,Hibernate 使用 active 作为默认列名。
  2. SoftDeleteType.DELETED
     该值 true 将记录标记为已删除,Hibernate 使用 deleted 作为默认列名。这是默认设置。

在这里,您可以看到将 SoftDeleteType 设置为 ACTIVE 的简单映射。

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE)
public class Account { ... }

当我现在执行与以前相同的测试时,Hibernate 会将实体的当前状态存储在 活动 列中。当我删除实体时,Hibernate 将该字段设置为 false ,并且所有查询仅返回 active=true 的记录。

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,active,id) values (?,true,?)

// find and remove an Account
10:46:26,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.active=true and a1_0.id=?
10:46:26,211 DEBUG SQL:135 - update Account set active=false where id=? and active=true

// query an Account
10:46:26,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

使用不同的列类型

Hibernate 的默认软删除实现使用布尔类型的列 来存储每条记录的当前状态。您可以通过提供 AttributeConverter 来更改此设置,该 AttributeConverter 将内部使用的 布尔值 映射到您选择的数据库类型。

如果您使用的是旧数据库,这将非常有用。它们有时使用表示每个记录的当前状态的 String 或枚举。

在这里,您可以看到一个实体映射,它告诉 Hibernate 将当前记录的状态存储在状态列中 state,并使用 StateConverter AttributeConverter 映射该属性。

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE, columnName = "state", converter = StateConverter.class)
public class Account { ... }

StateConverter 实现非常简单。它 实现 AttributeConverter 使用其 convertToDatabaseColumn 和 convertToEntityAttribute 方法

Hibernate 的软删除实现依赖于 一个布尔值 ,指示 记录是处于活动状态还是已删除状态 。因此, AttributeConverter 的第一个类型参数必须是 布尔值 。第二个 type 参数指定要存储在数据库中的类型。在此示例中,我想将 布尔值 映射到 字符串 “active”或“inactive”。

public class StateConverter implements AttributeConverter<Boolean, String>{

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute ? "active" : "inactive";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return dbData.equals("active");
    }
    
}

警告:如果将 布尔值 映射到 String , 则 Hibernate 的架构生成 会创建一个长度为 1 的 varchar 列。如果您 的 String 长度超过 1 个字符,我建议 您提供自己的数据库迁移 

当我现在执行与以前相同的测试时,Hibernate 将值“active”或“inactive”存储在 状态 列中。当我删除该实体时,Hibernate 将该字段设置为“非活动”,并且所有查询仅返回 state=active 的记录。

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,state,id) values (?,'active',?)

// find and remove an Account
11:18:39,857 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.state='active' and a1_0.id=?
11:18:39,867 DEBUG SQL:135 - update Account set state='inactive' where id=? and state='active'

// query an Account
11:18:39,889 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.state='active'

使用 Hibernate < 6.4 进行软删除

使用 Hibernate <6.4 实现软删除并不难,但它需要一些额外的工作。您必须:

  1. 告诉 Hibernate 在删除实体对象时执行 SQL UPDATE 而不是 DELETE 操作,并且
  2. 从查询结果中排除所有软删除的记录。

在以下示例中,我将向您展示如何轻松做到这一点。所有这些实体都将使用以下 Account 实体,该实体使用 AccountState 状态属性来指示帐户是 处于非 活动状态、 活动 状态还是 已删除 状态。

@Entity
@NamedQuery(name = "Account.FindByName", query = "SELECT a FROM Account a WHERE name like :name")
public class Account {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;
 
    @Column
    private String name;
 
    @Column
    @Enumerated(EnumType.STRING)
    private AccountState state;
 
    …
 
}

更新记录而不是删除它

要实现软删除,您需要覆盖 Hibernate 的默认删除操作。您可以使用 @SQLDelete 注释来做到这一点。此注释允许您定义自定义的本机 SQL 查询,Hibernate 将在您删除实体时执行该查询。您可以在以下代码片段中查看其示例。

@Entity
@SQLDelete(sql = "UPDATE account SET state = ‘DELETED’ WHERE id = ?", check = ResultCheckStyle.COUNT)
public class Account { … }

@SQLDelete 前面代码片段中的@SQLDelete注释告诉 Hibernate 执行给定的 SQL UPDATE 语句,而不是默认的 SQL DELETE 语句。它将帐户的状态更改为 DELETED ,您可以在 state 所有查询中使用 state 属性来排除已删除的帐户

Account a = em.find(Account.class, a.getId());
em.remove(a);
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> 'DELETED')
16:07:59,534 DEBUG SQL:92 – UPDATE account SET state = 'DELETED' WHERE id = ?

这就是创建基本软删除实现所需要做的全部操作。但是您还需要处理其他 2 件事:

  1. 删除 Account 实体时,Hibernate 不会在 更新其 state 当前会话中
  2. 您需要调整所有查询以排除已删除的实体。

更新当前会话中的状态属性

Hibernate 不会解析您提供给@SQLDelete注释的本机查询 @SQLDelete 。它只是设置绑定参数的值并执行它。因此,它不知道您向@SQLDelete注释提供了 SQL UPDATE 语句而不是 DELETE 语句 @SQLDelete 。在执行删除操作后,它也不知道 state 属性的值是否已过期。

在大多数情况下,这不是问题。当 Hibernate 执行 SQL 语句时,数据库记录将更新,并且所有查询都使用新的 状态 值。但是 的 Account ,您提供给 EntityManager.remove(Object entity) 操作

state 该实体的状态属性已过时。如果您在删除引用后立即释放引用,这没什么大不了的。在所有其他情况下,您应自行更新属性。

最简单的方法是使用生命周期回调,就像我在以下代码片段中所做的那样。 方法上的@PreRemove 注 deleteUser 指示 Hibernate 在执行删除操作之前调用此方法。我用它来将 state 属性的值设置为 DELETED。

@Entity
@SQLDelete(sql = "UPDATE account SET state = 'DELETED' WHERE id = ?", check = ResultCheckStyle.COUNT)
public class Account {
 
	...
	
	@PreRemove
	public void deleteUser() {
		this.state = AccountState.DELETED;
	}
 
}

在查询中排除软删除的实体

您需要检查 state 所有查询中的 state 属性,以便从查询结果中排除已删除的数据库记录。如果手动执行,则此任务容易出错,并且会强制您自己定义所有查询。 EntityManager.find(Class entityClass, Object primaryKey) 方法和 Hibernate Session 上的相应方法 Session 属性的语义 不知道 state ,也没有考虑它。

Hibernate’s @Where annotation provides a better way to exclude all deleted entities. It allows the definition of an SQL snippet, which Hibernate adds to the WHERE clause of all queries. The following code snippet shows a @Where annotation that excludes a record if its state is DELETED.

@Entity
@SQLDelete(sql = "UPDATE account SET state = 'DELETED' WHERE id = ?", check = ResultCheckStyle.COUNT)
@Where(clause = "state <> 'DELETED'")
@NamedQuery(name = "Account.FindByName", query = "SELECT a FROM Account a WHERE name like :name")
public class Account { ... }

As you can see in the following code snippets, Hibernate adds the defined WHERE clause when you perform a JPQL query or call the EntityManager.find(Class entityClass, Object primaryKey) method.

TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
Account a = q.getSingleResult();
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_, account0_.name as name2_0_, account0_.state as state3_0_ from Account account0_ where ( account0_.state <> 'DELETED') and (account0_.name like ?)
Account a = em.find(Account.class, a.getId());
16:07:59,540 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> 'DELETED')


转自:https://thorben-janssen.com/implement-soft-delete-hibernate