Introducing the Grails® 3 GORM Logical Delete Plugin
April 5, 2018
Recently our development team came upon the need to develop a Grails® 3 plugin that implements logical delete for entities.
A "logical" delete (sometimes referred to as a "soft" delete) is a delete that doesn’t actually delete the relevant data but instead marks the data as deleted. Marking the data as deleted has the benefit of excluding the data from queries by default, while still maintaining the ability to retrieve the data when/if necessary. Essentially the plugin offers a mechanism to "hide" domain objects from retrieval. This is useful for retaining data without having it clutter the current set of active data.
Logical Delete functionality is available in a few Grails 2 plugins. However these implementations rely on filters, ASTs, and runtime metaClass enhancements. We decided to create another implementation of the logical delete using some of latest Grails 3 features such as Traits and Listeners.
This blog will highlight how to use the plugin and also give some insight into the techniques used under the covers.
Summary
Logical delete of an entity has a few relevant use cases in enterprise applications. Companies may want to "delete" data from their everyday usage, but still keep it for later retrieval. Auditing requirements for financial institutions may require the data to be sustained for up to seven years.
From a technical perspective, if a domain model has several complex associations that make a chain of domain objects, hard deletion may cause slow cascading affects or be blocked by referential integrity. In these situations, a logical delete alleviates these concerns.
Installation
To add the GORM Logical Delete plugin to an application, add the following dependency to the dependencies block of your build.gradle
:
compile "org.grails.plugins:gorm-logical-delete:2.0.0.M2"
Enable Domain Entity with Logical Delete
Any domain entity can be implemented with the LogicalDelete
trait. The trait adds a boolean persistent property named deleted
to the domain class. The property is used to "hide" entities from queries if it is set to true
. Note the mapping columns can customize the property to a database column name.
import gorm.logical.delete.LogicalDelete
class Person implements LogicalDelete<Person> {
String userName
static mapping = {
// the deleted property may be configured
// like any other persistent property...
deleted column:"delFlag"
}
}
In order to delete a domain object enabled with logical delete, simply use the same GORM interface as usual.
Person p = new Person(userName: "Nirav").save()
p.delete()
If you would like to physically delete the record from persistence, use the attribute hard: true
:
p.delete(hard: true)
Undelete functionality is quite handy if you want to reverse the property to false.
Person.withDeleted {
Person p = Person.get(id)
p?.unDelete()
}
Querying Objects
When an object is enabled with logical delete, queries associated with the domain object will hide those marked with deleted = true
.
Dynamic Finders, Criteria Query, Detached Criteria Query, and the GormEntity
methods like get
, load
, proxy
, and read
are all supported.
See the Query documentation for a list of examples.
Note Hibernate Criteria and HQL queries are NOT supported by this plugin as they are ORM implementation specific.
Behind The Scenes
It's beneficial to understand how the plugin is implemented under the covers. We have tried to use the most efficient techniques with Groovy and the Grails framework, which reduces some of the noise and clutter found in previous plugin implementations.
In concept, what is occurring is that any domain object can be given a logical delete capability with the attribute deleted
. When the entity is logically deleted, it is set to true
. It is not physically deleted from persistence. During query time, a query event is intercepted by a listener and the query is altered to only include deleted = false
items from the result set. This gives the illusion that the items are not present, but in reality they are just hidden by the query.
LogicalDelete
The LogicalDelete
trait makes available a deleted
attribute. As stated earlier, an entity can implement this.
The LogicalDelete
trait has overridden static methods which take into account the deleted
property in an altered query. This allows the client to use the GormEntity
methods to essentially hide those items.
PreQueryListener
The PreQueryListener
implements ApplicationListener
and when the event fires, it will add a deleted
equals false
to the query.
The ThreadLocal
variable IGNORE_DELETED_FILTER
should also be noted. When set to true
, then all entities that exist in the database are included in the query. In other words, it bypasses the logical delete flag all together. In LogicalDelete
, note how this ThreadLocal
variable is used in the withDeleted
method. The method is passed in a closure (which is a query) and the variable is set to true
, which bypasses the delete flag.
WithDeletedTransformation
essentially achieves the same goal, but is intended as an implementation for an annotation.
The spock tests contained in the plugin are a good resource to reference examples and usages of the plugin API.
Take a look and we hope the plugin is useful for your needs.