Soft Deletion
In the Soft Deletion mode, the remove operation on JPA entities just marks database records as deleted instead of actually deleting them. Later a system administrator can either erase the records completely or restore them.
Soft deletion can help you to reduce the risk of data loss caused by incorrect user actions. It also allows users to make certain records inaccessible instantly even if there are references to them from other tables.
Soft deletion mechanism in Jmix is transparent for application developers. If you define the Soft Delete trait for an entity, the framework marks database records for deleted entity instances, and loads deleted instances according to the following rules:
-
Soft-deleted instances are not returned when loading by Id and are filtered out from results of JPQL queries.
-
In loaded entity graphs, soft-deleted instances are filtered out from collection attributes (To-Many references), but present in single-value attributes (To-One references).
For example, imagine a Customer - Order - OrderLine data model. Initially, an Order referenced a Customer and five instances of OrderLine. You have soft deleted the Customer instance and one of the OrderLine instances. Then if you load the Order together with Customer and collection of OrderLine, it will contain the reference to the deleted Customer and four OrderLine instances in the collection.
Handling of References
When a normal (hard deleted) entity is deleted, foreign keys in the database define handling of references to this entity. By default, you cannot delete an entity if it has references from other entities. To delete the referencing entity together with your entity, or to set the reference to null, you define ON DELETE CASCADE
or ON DELETE SET NULL
rules for the foreign key.
For soft deleted entities foreign keys also exist, but they cannot affect the deletion because there is no deletion from the database standpoint. So by default, when an entity instance is soft deleted, it doesn’t affect any linked entities.
Jmix offers @OnDelete
and @OnDeleteInverse
annotations to handle references between soft-deleted entities.
Studio entity designer contains visual hints to help you choose correct annotations and their values. |
-
@OnDelete
annotation specifies what to do with referenced entity when deleting the current entity. In the following example, allOrderLine
instances are deleted when the owningOrder
instance is deleted:public class Order { // ... @OnDelete(DeletePolicy.CASCADE) @Composition @OneToMany(mappedBy = "order") private List<OrderLine> lines;
-
@OnDeleteInverse
annotation specifies what to do with the current entity when deleting the referenced entity. In the following example, theCustomer
instance cannot be deleted if there is a reference to it from anOrder
instance:public class Order { // ... @OnDeleteInverse(DeletePolicy.DENY) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") private Customer customer;
The annotations can have one of the following values:
-
DeletePolicy.DENY
– to throw an exception on attempt to delete an entity if the reference is not null. -
DeletePolicy.CASCADE
– to delete the linked entities together. -
DeletePolicy.UNLINK
– to disconnect the linked entities by setting the reference attribute to null. Use this value only on the owning side of the association (the one with the@JoinColumn
annotation).
Unique Constraints
Soft deletion makes creation of database unique constraints more complicated. A constraint must take into account that there may be multiple records with the same value of the unique field: one non-deleted and any number of soft deleted.
The problem is solved differently for different databases. Follow the recommendations below and use the Indexes
tab of the Entity Designer to define unique constraints.
PostgreSQL
For PostgreSQL, we recommend using partial indexes.
Define a unique constraint for the desired column. The index definition in entity should look as follows:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL"})
})
Studio will generate the following Liquibase changelog:
<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>
Based on the changelog, Liquibase creates the partial index in the database:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL) where DELETED_DATE is null
Oracle and Microsoft SQL Server
Oracle and Microsoft SQL Server permit only one null value in a composite index. In this case, we recommend using a composite index including the DELETED_DATE
column.
Define a unique constraint for the desired column. The index definition in entity should look as follows:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL"})
})
Studio will generate the following Liquibase changelog:
<changeSet id="1" author="demo">
<createIndex indexName="IDX_CUSTOMER_UNQ_EMAIL" tableName="CUSTOMER" unique="true">
<column name="EMAIL"/>
<column name="DELETED_DATE"/>
</createIndex>
</changeSet>
Based on the changelog, Liquibase creates the composite index in the database:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL, DELETED_DATE)
MySQL and HSQL
For MySQL and HSQL, we recommend creating an additional non-null column and using a composite index which includes this column.
Create an additional attribute and make sure it is updated from the deletedDate
setter:
@SystemLevel
@Column(name = "DELETED_DATE_NN")
@Temporal(TemporalType.TIMESTAMP)
private Date deletedDateNN = new Date(0); // add initializer manually
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); // add this manually
}
Define a unique constraint including DELETED_DATE_NN
column. The index definition in entity should look as follows:
@Table(name = "CUSTOMER", uniqueConstraints = {
@UniqueConstraint(name = "IDX_CUSTOMER_UNQ_EMAIL", columnNames = {"EMAIL", "DELETED_DATE_NN"})
})
Studio will generate the following Liquibase changelog:
<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>
Based on the changelog, Liquibase creates the composite index in the database:
create unique index IDX_CUSTOMER_UNQ_EMAIL on CUSTOMER (EMAIL, DELETED_DATE_NN)
Turning Soft Deletion Off
By default, soft deletion is on for all entities having the Soft Delete trait. But you can turn it off for a particular operation using the PersistenceHints.SOFT_DELETION
hint with the false
value.
-
When loading entities using
DataManager
:@Autowired private DataManager dataManager; public Customer loadHardDeletedCustomer(Id<Customer> customerId) { return dataManager.load(customerId).hint(PersistenceHints.SOFT_DELETION, false).one(); }
Results will include soft deleted instances.
-
When removing entities using
DataManager
:@Autowired private DataManager dataManager; public void hardDeleteCustomer(Customer customer) { dataManager.save( new SaveContext() .removing(customer) .setHint(PersistenceHints.SOFT_DELETION, false) ); }
-
When working with
EntityManager
:@PersistenceContext private EntityManager entityManager; @Transactional public void hardRemoveCustomerByEM(Customer customer) { entityManager.setProperty(PersistenceHints.SOFT_DELETION, false); entityManager.remove(customer); }