1. Introduction

The GORM Logical Delete plugin provides support for utilizing logical deletes for GORM managed entities in a Grails 3 app.

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.

2. 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.BUILD-SNAPSHOT"

Snapshots are now published automatically to Artifactory OSS on every successful build. You can use them by defining this Maven repository inside the repositories block in your build.gradle:

maven { url "https://oss.jfrog.org/artifactory/oss-snapshot-local" }

3. LogicalDelete Trait

The plugin provides the gorm.logical.delete.LogicalDelete trait which can applied to any domain class to indicate that the domain class should participate in logical deletes. The trait adds a boolean persistent property named deleted to the domain class. When this property has a value of true, it indicates that the record has been logically deleted and as such will be excluded from query results by default.

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"
    }
}

4. Deleting Objects

Deleting a domain object that implements the gorm.logical.delete.LogicalDelete trait is simple as calling delete(). This will mark the deleted field to true and all queries for that domain object will hide the item from its result set.

Person p = new Person(userName: "Nirav").save(flush: true)
p.delete(flush:true)

In order to reverse the deleted field use unDelete(). . .

p.unDelete(flush: true)

If you would like to physically delete the record from persistence, use the attribute hard: true:

p.delete(hard: true)

5. Queries

Most queries will by default automatically exclude logically deleted records from their results.

A dynamic finder query authored like this…​

results = Person.findAll {
    userName == 'Robert'
}

Is executed as if it had been written like this…​

results = Person.findAll {
    userName == 'Robert'
    deleted == false
}

For cases where you would like logically deleted records to be included in query results, queries may be wrapped in a call to withDeleted as shown below. Any query can be wrapped with the withDeleted closure.

results = Person.withDeleted { Person.findAll() }

The plugin supports a variety of query types in addition to dynamic finders, see examples below:

5.1. Criteria Query

def criteria = Person.createCriteria()
def results = criteria {
    or {
        eq("userName", "Ben")
        eq("userName", "Nirav")
    }
}

5.2. Detached Criteria Query

DetachedCriteria<Person> query = Person.where {
    userName == "Ben" || userName == "Nirav"
}
def results = query.list()

5.3. GORM methods

The standard GormEntity<D> methods of get, load, proxy, read are also supported for logical delete.

Hibernate Criteria and HQL queries are NOT supported by this plugin.

5.4. GORM Dataservices

GORM Dataservices are supported for logical delete.

6. WithDeleted Annotation

The @WithDeleted annotation allows you to mark a method on a class so that queries within the method will retrieve logically deleted entities. This is an alternate solution to using the withDeleted closure. Ths is useful in conjuction for Gorm Data Services, or any classes that act as DAOs.

@Service(Person)
interface PersonService {

    List<Person> listPeople()

    @WithDeleted
    List<Person> listPeopleWithDeleted()

    void delete(Serializable id)
}

7. Implementation Details

This is a brief overview on the implementation of the gorm-logical-delete plugin.

7.1. LogicalDelete Trait

The gorm.logical.delete.LogicalDelete trait establishes the deleted field and also overrides methods that will implement the query and delete functionality.

7.2. PreQueryListener

The gorm.logical.delete.PreQueryListener listens for a query event and appropriately adds to the existing query in order to hide deleted items.

7.3. Spock Tests

The spock tests contained in the plugin are a good resource to reference examples and usages of the plugin API.

8. API Documentation

API documentation may be found here.