在某些应用程序中,您不希望或不允许从数据库中永久删除记录。但是,您仍然需要删除或隐藏不再处于活动状态的记录。例如,您希望保留一个用户帐户,因为它链接到仍在使用的其他业务对象。
您有 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 ,用于确定数据库中存储的布尔值的含义:
- SoftDeleteType.ACTIVE。
该值 true 将记录标记为活动状态,Hibernate 使用 active 作为默认列名。 - 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 实现软删除并不难,但它需要一些额外的工作。您必须:
- 告诉 Hibernate 在删除实体对象时执行 SQL UPDATE 而不是 DELETE 操作,并且
- 从查询结果中排除所有软删除的记录。
在以下示例中,我将向您展示如何轻松做到这一点。所有这些实体都将使用以下 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 件事:
- 删除 Account 实体时,Hibernate 不会在 更新其 state 当前会话中
- 您需要调整所有查询以排除已删除的实体。
更新当前会话中的状态属性
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