软删除
在 Soft Deletion 模式下,对 JPA 实体的删除操作只是将数据库记录标记为已删除,实际上并没有删除。之后,系统管理员可以彻底删除或恢复这些数据。
软删除可以帮助降低因用户错误操作而导致数据丢失的风险。另外,即使其他表引用了某些记录,软删除也可以让这些记录立即变为不可访问状态。
Jmix 中的软删除机制对应用程序开发人员是透明的。如果为实体定义了 软删除特性,框架会为在数据库标记已删除的实体实例,并根据以下规则加载已删除的实例:
-
按 Id 加载时不返回软删除的实例,从 JPQL 查询的结果中过滤掉。
-
在加载的实体关系图中,软删除的实例会从集合属性(To-Many 引用)中过滤掉,但会存在于单值属性(To-One 引用)中。
例如,想象一个 Customer - Order - OrderLine 数据模型。最初,一个 Order 引用了一个 Customer 和 五个 OrderLine 实例。你软删除了 Customer 实例和一个 OrderLine 实例。然后,如果你将 Order、Customer 和 OrderLine 集合一起加载,将包含已删除的 Customer 和 四个 OrderLine 实例的引用。
对引用的处理
当一个普通的(硬删除的)实体被删除时,由数据库中的外键定义对该实体引用的处理。默认情况下,如果一个实体被其他实体引用,则无法被删除。如需将引用的实体与要删除的实体一起删除,或将引用设置为 null,则需要为外键定义 ON DELETE CASCADE
或 ON DELETE SET NULL
规则。
对于软删除的实体,外键也存在,但它们不会影响删除,因为从数据库的角度来看没有删除。所以默认情况下,当实体实例被软删除时,不会影响任何有关联的实体。
Jmix 提供了 @OnDelete
和 @OnDeleteInverse
注解来处理软删除实体之间的引用。
Studio 实体设计器有提示可以帮助你选择正确的注解及其值。 |
-
@OnDelete
注解指定在删除当前实体时如何处理引用的实体。在下面的例子中,当拥有方Order
实例被删除时,所有的OrderLine
实例都会被删除:public class Order { // ... @OnDelete(DeletePolicy.CASCADE) @Composition @OneToMany(mappedBy = "order") private List<OrderLine> lines;
-
@OnDeleteInverse
注解指定删除引用的实体时如何处理当前实体。在以下示例中,如果Order
实例中有对Customer
实例的引用,则无法删除Customer
:public class Order { // ... @OnDeleteInverse(DeletePolicy.DENY) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") private Customer customer;
注解值可以有以下三种:
-
DeletePolicy.DENY
- 如果引用不为空,则在尝试删除实体时抛出异常。 -
DeletePolicy.CASCADE
- 一并删除关联的实体。 -
DeletePolicy.UNLINK
- 通过将引用属性设置为空来断开关联。仅在关联的拥有方(带有@JoinColumn
注解的一方)使用此值。
唯一性约束
软删除使创建数据库唯一约束更加复杂。约束必须考虑到可能有多个记录具有相同的唯一字段值:一个未删除和任意数量的软删除记录。
对于不同的数据库,该问题的解决方式不同。按照下面的建议并使用 实体设计器 的 Indexes
标签页定义唯一约束。
PostgreSQL
对于 PostgreSQL,我们推荐使用部分索引(partial indexes)。
为需要的列定义唯一约束。实体中的索引定义示例:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL"})
})
Studio 会生成下列 Liquibase 更改日志:
<changeSet id="1" author="demo" dbms="postgresql">
<createIndex indexName="IDX_CUSTOMER_UNQ_EMAIL" tableName="CUSTOMER" unique="true">
<column name="EMAIL"/>
</createIndex>
<modifySql>
<append value="where DELETED_DATE is null"/>
</modifySql>
</changeSet>
根据更改日志,Liquibase 会在数据库创建部分索引:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL) where DELETED_DATE is null
Oracle 和 Microsoft SQL Server
Oracle 和 Microsoft SQL Server 仅支持组合索引中有一个 null 值。因此,我们推荐使用包含 DELETED_DATE
列的组合索引。
为需要的列定义唯一约束。实体中的索引定义示例:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL"})
})
Studio 会生成下列 Liquibase 更改日志:
<changeSet id="1" author="demo">
<createIndex indexName="IDX_CUSTOMER_UNQ_EMAIL" tableName="CUSTOMER" unique="true">
<column name="EMAIL"/>
<column name="DELETED_DATE"/>
</createIndex>
</changeSet>
根据更改日志,Liquibase 会在数据库创建组合索引:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL, DELETED_DATE)
MySQL 和 HSQL
对于 MySQL 和 HSQL,我们建议创建一个额外的非 null 列,并使用包含该列的组合索引。
创建一个额外的属性,并确保通过 deletedDate
setter 更新:
@SystemLevel
@Column(name = "DELETED_DATE_NN")
@Temporal(TemporalType.TIMESTAMP)
private Date deletedDateNN = new Date(0); // 手动初始化
public Date getDeletedDateNN() {
return deletedDateNN;
}
public void setDeletedDateNN(Date deletedDateNN) {
this.deletedDateNN = deletedDateNN;
}
public void setDeletedDate(Date deletedDate) {
this.deletedDate = deletedDate;
setDeletedDateNN(deletedDate == null ? new Date(0) : deletedDate); // 手动添加这些代码
}
定义包含 DELETED_DATE_NN
列的唯一约束。实体中的索引定义示例:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL", "DELETED_DATE_NN"})
})
Studio 会生成下列 Liquibase 更改日志:
<changeSet id="1" author="demo">
<createIndex indexName="IDX_CUSTOMER_UNQ_EMAIL" tableName="CUSTOMER" unique="true">
<column name="EMAIL"/>
<column name="DELETED_DATE_NN"/>
</createIndex>
</changeSet>
根据更改日志,Liquibase 会在数据库创建组合索引:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL, DELETED_DATE_NN)
关闭软删除
默认情况下,对所有具有 软删除特性 的实体启用软删除。但是你可以使用带有 false
值的 PersistenceHints.SOFT_DELETION
hint 为特定的操作关闭它。
-
使用
DataManager
加载实体时:@Autowired private DataManager dataManager; public Customer loadHardDeletedCustomer(Id<Customer> customerId) { return dataManager.load(customerId).hint(PersistenceHints.SOFT_DELETION, false).one(); }
结果中将会包含软删除的实例。
-
使用
DataManager
删除实体时:@Autowired private DataManager dataManager; public void hardDeleteCustomer(Customer customer) { dataManager.save( new SaveContext() .removing(customer) .setHint(PersistenceHints.SOFT_DELETION, false) ); }
-
使用
EntityManager
时:@PersistenceContext private EntityManager entityManager; @Transactional public void hardRemoveCustomerByEM(Customer customer) { entityManager.setProperty(PersistenceHints.SOFT_DELETION, false); entityManager.remove(customer); }