From 161928a1c3dd4e5d230c34aaac68c857b44cdece Mon Sep 17 00:00:00 2001 From: benrhine Date: Mon, 2 Apr 2018 13:22:34 -0400 Subject: [PATCH 1/9] adding 2 new traits to allow logical deleted field to be a date or a string with associated tests --- .../logical/delete/DateLogicalDelete.groovy | 103 +++++++ .../logical/delete/PreQueryListener.groovy | 8 + .../logical/delete/StringLogicalDelete.groovy | 103 +++++++ .../logical/delete/DateCriteriaSpec.groovy | 63 +++++ .../delete/DateDetachedCriteriaSpec.groovy | 68 +++++ .../delete/DateDynamicFindersSpec.groovy | 91 +++++++ .../delete/DateLogicalDeleteSpec.groovy | 254 ++++++++++++++++++ .../logical/delete/DateWithDeletedSpec.groovy | 112 ++++++++ .../logical/delete/StringCriteriaSpec.groovy | 62 +++++ .../delete/StringDetachedCriteriaSpec.groovy | 68 +++++ .../delete/StringDynamicFindersSpec.groovy | 91 +++++++ .../delete/StringLogicalDeleteSpec.groovy | 254 ++++++++++++++++++ .../delete/StringWithDeletedSpec.groovy | 113 ++++++++ .../gorm/logical/delete/test/Person2.groovy | 19 ++ .../delete/test/Person2TestData.groovy | 32 +++ .../gorm/logical/delete/test/Person3.groovy | 19 ++ .../delete/test/Person3TestData.groovy | 32 +++ 17 files changed, 1492 insertions(+) create mode 100644 src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy create mode 100644 src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy create mode 100644 src/test/groovy/gorm/logical/delete/DateCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/DateDetachedCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/DateDynamicFindersSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/DateLogicalDeleteSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/DateWithDeletedSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/StringCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/StringDetachedCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/StringDynamicFindersSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/StringLogicalDeleteSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/StringWithDeletedSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person2.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person2TestData.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person3.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person3TestData.groovy diff --git a/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy new file mode 100644 index 0000000..2ff4e48 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy @@ -0,0 +1,103 @@ +package gorm.logical.delete + +import grails.gorm.DetachedCriteria +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.gorm.GormStaticApi + +import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER + +trait DateLogicalDelete extends GormEntity { + Date deleted = null + + static Object withDeleted(Closure closure) { + final initialThreadLocalValue = IGNORE_DELETED_FILTER.get() + try { + IGNORE_DELETED_FILTER.set(true) + return closure.call() + } finally { + IGNORE_DELETED_FILTER.set(initialThreadLocalValue) + } + } + + static D get(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().get(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D read(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().read(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D load(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().load(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D proxy(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().proxy(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + void delete() { + final Date date = new Date() + this.markDirty('deleted', date, null) + this.deleted = date + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + final Date date = new Date() + this.markDirty('deleted', date, null) + this.deleted = date + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', null) + this.deleted = null + save(params) + } + + /** ============================================================================================ + * Private Methods: + * ============================================================================================= */ + private static GormStaticApi currentGormStaticApi() { + (GormStaticApi)GormEnhancer.findStaticApi(this) + } +} \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy index 914793c..c880fc9 100644 --- a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy +++ b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy @@ -41,6 +41,14 @@ class PreQueryListener implements ApplicationListener { query.eq('deleted', false) } } + + if (DateLogicalDelete.isAssignableFrom(entity.javaClass) || StringLogicalDelete.isAssignableFrom(entity.javaClass)) { + log.debug "This entity [${entity.javaClass}] implements logical delete" + + if (!IGNORE_DELETED_FILTER.get()) { + query.eq('deleted', null) + } + } } catch (Exception e) { log.error(e.message) } diff --git a/src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy new file mode 100644 index 0000000..8022f83 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy @@ -0,0 +1,103 @@ +package gorm.logical.delete + +import grails.gorm.DetachedCriteria +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.gorm.GormStaticApi + +import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER + +@CompileStatic +trait StringLogicalDelete extends GormEntity { + String deleted = null + + static Object withDeleted(Closure closure) { + final initialThreadLocalValue = IGNORE_DELETED_FILTER.get() + try { + IGNORE_DELETED_FILTER.set(true) + return closure.call() + } finally { + IGNORE_DELETED_FILTER.set(initialThreadLocalValue) + } + } + + static D get(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().get(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D read(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().read(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D load(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().load(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + static D proxy(final Serializable id) { + if (IGNORE_DELETED_FILTER.get()) { + this.currentGormStaticApi().proxy(id) + } else { + new DetachedCriteria(this).build { + eq 'id', id + eq 'deleted', null + }.get() + } + } + + void delete(String newValue) { + this.markDirty('deleted', newValue, null) + this.deleted = newValue + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', params?.newValue, null) + this.deleted = (String) params?.newValue + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null,) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', null) + this.deleted = null + save(params) + } + + /** ============================================================================================ + * Private Methods: + * ============================================================================================= */ + private static GormStaticApi currentGormStaticApi() { + (GormStaticApi)GormEnhancer.findStaticApi(this) + } +} diff --git a/src/test/groovy/gorm/logical/delete/DateCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/DateCriteriaSpec.groovy new file mode 100644 index 0000000..f577f0a --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateCriteriaSpec.groovy @@ -0,0 +1,63 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person +import gorm.logical.delete.test.Person2 +import gorm.logical.delete.test.Person2TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class DateCriteriaSpec extends Specification implements DomainUnitTest, Person2TestData { + + /******************* test criteria ***********************************/ + + @Rollback + void 'test criteria - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + // tag::criteria_query[] + def criteria = Person2.createCriteria() + def results = criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + // end::criteria_query[] + + then: "we should not get anything bc they were deleted" + !results + + when: + results = criteria { + eq("userName", "Jeff") + } + + then: + results + results[0].userName == 'Jeff' + } + + /******************* test criteria with projection ***********************************/ + + @Rollback + void 'test criteria with projection - logical deleted items'() { + // projection Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + def criteria = Person2.createCriteria() + def results = criteria.get { + projections { + count() + } + } + + then: "we should not get the deleted items" + results == 1 + } +} diff --git a/src/test/groovy/gorm/logical/delete/DateDetachedCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/DateDetachedCriteriaSpec.groovy new file mode 100644 index 0000000..bdaebc7 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateDetachedCriteriaSpec.groovy @@ -0,0 +1,68 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person2 +import gorm.logical.delete.test.Person2TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class DateDetachedCriteriaSpec extends Specification implements DomainUnitTest, Person2TestData { + + /******************* test where ***********************************/ + + @Rollback + void 'test detached criteria where - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + // tag::detachedCriteria_query[] + DetachedCriteria query = Person2.where { + userName == "Ben" || userName == "Nirav" + } + def results = query.list() + // end::detachedCriteria_query[] + then: "we should not get anything bc they were deleted" + !results + + when: + query = Person2.where { + userName == "Jeff" + } + results = query.find() + + then: + results + results.userName == 'Jeff' + } + + /******************* test findall ***********************************/ + + @Rollback + void 'test detached criteria findAll - logical deleted items'() { + // findAll detachedCriteria Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + def results = Person2.findAll { + userName == "Ben" || userName == "Nirav" + } + + then: "we should not get anything bc they were deleted" + !results + + when: + results = Person2.findAll { + userName == "Jeff" + } + + then: + results + results[0].userName == 'Jeff' + } + + /********************* setup *****************************/ +} diff --git a/src/test/groovy/gorm/logical/delete/DateDynamicFindersSpec.groovy b/src/test/groovy/gorm/logical/delete/DateDynamicFindersSpec.groovy new file mode 100644 index 0000000..5479956 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateDynamicFindersSpec.groovy @@ -0,0 +1,91 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person2 +import gorm.logical.delete.test.Person2TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class DateDynamicFindersSpec extends Specification implements DomainUnitTest, Person2TestData { + + /******************* test FindAll ***********************************/ + + @Rollback + void 'test dynamic findAll hide logical deleted items'() { + // findAll() Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + List results = Person2.findAll() + + then: "we should only get those not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + + // list() calll + when: + results.clear() + results = Person2.list() + + then: + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test findBy ***************************/ + + @Rollback + void 'test dynamic findByUserName hide logical deleted items'() { + // findByUserName() Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + Person2 result1 = Person2.findByUserName("Ben") + Person2 result2 = Person2.findByUserName("Nirav") + + then: "we shouldn't get any bc it was deleted" + !result1 + !result2 + } + + /***************** test findByDeleted ***************************/ + + @Rollback + void 'test dynamic findByDeleted hide logical deleted items'() { + // findByDeleted() Call + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + List results = Person2.findAllByDeletedIsNotNull() + + then: "we should not get any because these are logically deleted" + results.size() == 0 + results.clear() + + when: + results = Person2.findAllByDeleted(null) + + then: "we should find the entity because it is not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test get() ***************************/ + + @Rollback + void 'test dynamic get() finds logical deleted items'() { + when: "when 'get()' is used, we cannot access logically deleted entities" + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + final Person2 ben = Person2.get(1) + final Person2 nirav = Person2.get(2) + + then: + !nirav + !ben + } +} diff --git a/src/test/groovy/gorm/logical/delete/DateLogicalDeleteSpec.groovy b/src/test/groovy/gorm/logical/delete/DateLogicalDeleteSpec.groovy new file mode 100644 index 0000000..1bafa36 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateLogicalDeleteSpec.groovy @@ -0,0 +1,254 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person2 +import gorm.logical.delete.test.Person2TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class DateLogicalDeleteSpec extends Specification implements DomainUnitTest, Person2TestData { + + /******************* delete tests - (w/ get) ***********************************/ + + @Rollback + void 'test logical delete flush - get'() { + when: + Person2 p = Person2.get(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - get'() { + when: + Person2 p = Person2.get(1) + + then: + !p.deleted + + when: + p.delete() + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - get'() { + when: + Person2 p = Person2.get(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person2.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ load) ***********************************/ + + @Rollback + void 'test logical delete flush - load'() { + when: + Person2 p = Person2.load(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person2.withDeleted { Person2.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - load'() { + when: + Person2 p = Person2.load(1) + + then: + !p.deleted + + when: + p.delete() + p = Person2.withDeleted { Person2.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - load'() { + when: + Person2 p = Person2.load(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person2.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ proxy) ***********************************/ + + @Rollback + void 'test logical delete flush - proxy'() { + when: + Person2 p = Person2.proxy(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person2.withDeleted { Person2.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - proxy'() { + when: + Person2 p = Person2.proxy(1) + + then: + !p.deleted + + when: + p.delete() + p = Person2.withDeleted { Person2.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - proxy'() { + when: + Person2 p = Person2.proxy(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person2.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ read) ***********************************/ + + @Rollback + void 'test logical delete flush - read'() { + when: + Person2 p = Person2.read(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person2.withDeleted { Person2.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - read'() { + when: + Person2 p = Person2.read(1) + + then: + !p.deleted + + when: + p.delete() + p = Person2.withDeleted { Person2.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - read'() { + when: + Person2 p = Person2.read(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person2.count() == 2 // 2 left after one hard deleted + } + + /******************* undelete tests ***********************************/ + + @Rollback + void 'test logical unDelete flush'() { + when: + Person2 p = Person2.get(1) + p.delete() + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + + when: + p.unDelete(flush: true) + p = Person2.get(1) + + then: + !p.deleted + + } + + @Rollback + void 'test logical unDelete'() { + when: + Person2 p = Person2.get(1) + p.delete() + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + + when: + p.unDelete() + p = Person2.get(1) + + then: + !p.deleted + + } +} diff --git a/src/test/groovy/gorm/logical/delete/DateWithDeletedSpec.groovy b/src/test/groovy/gorm/logical/delete/DateWithDeletedSpec.groovy new file mode 100644 index 0000000..b62487c --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateWithDeletedSpec.groovy @@ -0,0 +1,112 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person2 +import gorm.logical.delete.test.Person2TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class DateWithDeletedSpec extends Specification implements DomainUnitTest, Person2TestData { + + /******************* test with delete ***********************************/ + + @Rollback + void 'test withDeleted findAll - logical deleted items'() { + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + def results = Person2.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + + // tag::find_all_with_deleted[] + results = Person2.withDeleted { Person2.findAll() } + // end::find_all_with_deleted[] + + then: + results.size() == 3 + } + + @Rollback + void 'test withDeleted detached criteria'() { + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + DetachedCriteria query = Person2.where { + userName == "Ben" || userName == "Nirav" + } + def results = Person2.withDeleted { + query.list() + } + + then: "we should get deleted items" + results.size() == 2 + } + + @Rollback + void 'test withDeleted criteria'() { + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + def criteria = Person2.createCriteria() + def results = Person2.withDeleted { + criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + } + + then: "we should get deleted items" + results.size() == 2 + + } + + void 'test that the thread local is restored to false even if the closure throws an exception'() { + when: + Person2.withDeleted { + throw new IllegalStateException() + } + + then: + thrown IllegalStateException + + and: + !PreQueryListener.IGNORE_DELETED_FILTER.get() + } + + void 'test that nested .withDeleted calls work as expected'() { + // One wouldn't directly nest calls to withDeleted intentionally + // but a service method could use withDeleted and invoke another service + // method which also invokes with deleted, and that could cause a problem + when: + assert Person2.count() == 3 + Person2.findByUserName("Ben").delete() + Person2.findByUserName("Nirav").delete() + def results = Person2.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + results = Person2.withDeleted { + Person2.withDeleted {} + + // make sure the filter is still working after the previous call + // to withDeletedl... + Person2.findAll() + } + + then: + results.size() == 3 + + } +} diff --git a/src/test/groovy/gorm/logical/delete/StringCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/StringCriteriaSpec.groovy new file mode 100644 index 0000000..ff593e7 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringCriteriaSpec.groovy @@ -0,0 +1,62 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person3 +import gorm.logical.delete.test.Person3TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class StringCriteriaSpec extends Specification implements DomainUnitTest, Person3TestData { + + /******************* test criteria ***********************************/ + + @Rollback + void 'test criteria - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + // tag::criteria_query[] + def criteria = Person3.createCriteria() + def results = criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + // end::criteria_query[] + + then: "we should not get anything bc they were deleted" + !results + + when: + results = criteria { + eq("userName", "Jeff") + } + + then: + results + results[0].userName == 'Jeff' + } + + /******************* test criteria with projection ***********************************/ + + @Rollback + void 'test criteria with projection - logical deleted items'() { + // projection Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + def criteria = Person3.createCriteria() + def results = criteria.get { + projections { + count() + } + } + + then: "we should not get the deleted items" + results == 1 + } +} diff --git a/src/test/groovy/gorm/logical/delete/StringDetachedCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/StringDetachedCriteriaSpec.groovy new file mode 100644 index 0000000..8cd061d --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringDetachedCriteriaSpec.groovy @@ -0,0 +1,68 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person3 +import gorm.logical.delete.test.Person3TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class StringDetachedCriteriaSpec extends Specification implements DomainUnitTest, Person3TestData { + + /******************* test where ***********************************/ + + @Rollback + void 'test detached criteria where - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + // tag::detachedCriteria_query[] + DetachedCriteria query = Person3.where { + userName == "Ben" || userName == "Nirav" + } + def results = query.list() + // end::detachedCriteria_query[] + then: "we should not get anything bc they were deleted" + !results + + when: + query = Person3.where { + userName == "Jeff" + } + results = query.find() + + then: + results + results.userName == 'Jeff' + } + + /******************* test findall ***********************************/ + + @Rollback + void 'test detached criteria findAll - logical deleted items'() { + // findAll detachedCriteria Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + def results = Person3.findAll { + userName == "Ben" || userName == "Nirav" + } + + then: "we should not get anything bc they were deleted" + !results + + when: + results = Person3.findAll { + userName == "Jeff" + } + + then: + results + results[0].userName == 'Jeff' + } + + /********************* setup *****************************/ +} diff --git a/src/test/groovy/gorm/logical/delete/StringDynamicFindersSpec.groovy b/src/test/groovy/gorm/logical/delete/StringDynamicFindersSpec.groovy new file mode 100644 index 0000000..d73de81 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringDynamicFindersSpec.groovy @@ -0,0 +1,91 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person3 +import gorm.logical.delete.test.Person3TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class StringDynamicFindersSpec extends Specification implements DomainUnitTest, Person3TestData { + + /******************* test FindAll ***********************************/ + + @Rollback + void 'test dynamic findAll hide logical deleted items'() { + // findAll() Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + List results = Person3.findAll() + + then: "we should only get those not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + + // list() calll + when: + results.clear() + results = Person3.list() + + then: + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test findBy ***************************/ + + @Rollback + void 'test dynamic findByUserName hide logical deleted items'() { + // findByUserName() Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + Person3 result1 = Person3.findByUserName("Ben") + Person3 result2 = Person3.findByUserName("Nirav") + + then: "we shouldn't get any bc it was deleted" + !result1 + !result2 + } + + /***************** test findByDeleted ***************************/ + + @Rollback + void 'test dynamic findByDeleted hide logical deleted items'() { + // findByDeleted() Call + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + List results = Person3.findAllByDeleted('test') + + then: "we should not get any because these are logically deleted" + results.size() == 0 + results.clear() + + when: + results = Person3.findAllByDeleted(null) + + then: "we should find the entity because it is not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test get() ***************************/ + + @Rollback + void 'test dynamic get() finds logical deleted items'() { + when: "when 'get()' is used, we cannot access logically deleted entities" + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + final Person3 ben = Person3.get(1) + final Person3 nirav = Person3.get(2) + + then: + !nirav + !ben + } +} diff --git a/src/test/groovy/gorm/logical/delete/StringLogicalDeleteSpec.groovy b/src/test/groovy/gorm/logical/delete/StringLogicalDeleteSpec.groovy new file mode 100644 index 0000000..57f9e29 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringLogicalDeleteSpec.groovy @@ -0,0 +1,254 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person3 +import gorm.logical.delete.test.Person3TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class StringLogicalDeleteSpec extends Specification implements DomainUnitTest, Person3TestData { + + /******************* delete tests - (w/ get) ***********************************/ + + @Rollback + void 'test logical delete flush - get'() { + when: + Person3 p = Person3.get(1) + + then: + !p.deleted + + when: + p.delete(newValue: 'test', flush:true) + p.discard() + p = Person3.withDeleted { Person3.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - get'() { + when: + Person3 p = Person3.get(1) + + then: + !p.deleted + + when: + p.delete('test') + p = Person3.withDeleted { Person3.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - get'() { + when: + Person3 p = Person3.get(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person3.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ load) ***********************************/ + + @Rollback + void 'test logical delete flush - load'() { + when: + Person3 p = Person3.load(1) + + then: + !p.deleted + + when: + p.delete(newValue: 'test', flush:true) + p.discard() + p = Person3.withDeleted { Person3.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - load'() { + when: + Person3 p = Person3.load(1) + + then: + !p.deleted + + when: + p.delete('test') + p = Person3.withDeleted { Person3.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - load'() { + when: + Person3 p = Person3.load(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person3.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ proxy) ***********************************/ + + @Rollback + void 'test logical delete flush - proxy'() { + when: + Person3 p = Person3.proxy(1) + + then: + !p.deleted + + when: + p.delete(newValue: 'test', flush:true) + p.discard() + p = Person3.withDeleted { Person3.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - proxy'() { + when: + Person3 p = Person3.proxy(1) + + then: + !p.deleted + + when: + p.delete('test') + p = Person3.withDeleted { Person3.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - proxy'() { + when: + Person3 p = Person3.proxy(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person3.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ read) ***********************************/ + + @Rollback + void 'test logical delete flush - read'() { + when: + Person3 p = Person3.read(1) + + then: + !p.deleted + + when: + p.delete(newValue: 'test', flush:true) + p.discard() + p = Person3.withDeleted { Person3.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - read'() { + when: + Person3 p = Person3.read(1) + + then: + !p.deleted + + when: + p.delete('test') + p = Person3.withDeleted { Person3.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - read'() { + when: + Person3 p = Person3.read(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person3.count() == 2 // 2 left after one hard deleted + } + + /******************* undelete tests ***********************************/ + + @Rollback + void 'test logical unDelete flush'() { + when: + Person3 p = Person3.get(1) + p.delete('test') + p = Person3.withDeleted { Person3.get(1) } + + then: + p.deleted + + when: + p.unDelete(flush: true) + p = Person3.get(1) + + then: + !p.deleted + + } + + @Rollback + void 'test logical unDelete'() { + when: + Person3 p = Person3.get(1) + p.delete('test') + p = Person3.withDeleted { Person3.get(1) } + + then: + p.deleted + + when: + p.unDelete() + p = Person3.get(1) + + then: + !p.deleted + + } +} diff --git a/src/test/groovy/gorm/logical/delete/StringWithDeletedSpec.groovy b/src/test/groovy/gorm/logical/delete/StringWithDeletedSpec.groovy new file mode 100644 index 0000000..37de49a --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringWithDeletedSpec.groovy @@ -0,0 +1,113 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person +import gorm.logical.delete.test.Person3 +import gorm.logical.delete.test.Person3TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class StringWithDeletedSpec extends Specification implements DomainUnitTest, Person3TestData { + + /******************* test with delete ***********************************/ + + @Rollback + void 'test withDeleted findAll - logical deleted items'() { + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + def results = Person3.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + + // tag::find_all_with_deleted[] + results = Person3.withDeleted { Person3.findAll() } + // end::find_all_with_deleted[] + + then: + results.size() == 3 + } + + @Rollback + void 'test withDeleted detached criteria'() { + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + DetachedCriteria query = Person3.where { + userName == "Ben" || userName == "Nirav" + } + def results = Person3.withDeleted { + query.list() + } + + then: "we should get deleted items" + results.size() == 2 + } + + @Rollback + void 'test withDeleted criteria'() { + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + def criteria = Person3.createCriteria() + def results = Person3.withDeleted { + criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + } + + then: "we should get deleted items" + results.size() == 2 + + } + + void 'test that the thread local is restored to false even if the closure throws an exception'() { + when: + Person3.withDeleted { + throw new IllegalStateException() + } + + then: + thrown IllegalStateException + + and: + !PreQueryListener.IGNORE_DELETED_FILTER.get() + } + + void 'test that nested .withDeleted calls work as expected'() { + // One wouldn't directly nest calls to withDeleted intentionally + // but a service method could use withDeleted and invoke another service + // method which also invokes with deleted, and that could cause a problem + when: + assert Person3.count() == 3 + Person3.findByUserName("Ben").delete('test') + Person3.findByUserName("Nirav").delete('test') + def results = Person3.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + results = Person3.withDeleted { + Person3.withDeleted {} + + // make sure the filter is still working after the previous call + // to withDeletedl... + Person3.findAll() + } + + then: + results.size() == 3 + + } +} diff --git a/src/test/groovy/gorm/logical/delete/test/Person2.groovy b/src/test/groovy/gorm/logical/delete/test/Person2.groovy new file mode 100644 index 0000000..f65ea1c --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person2.groovy @@ -0,0 +1,19 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.DateLogicalDelete +import grails.gorm.annotation.Entity + +@Entity +class Person2 implements DateLogicalDelete { + String userName + + static mapping = { + // the deleted property may be configured + // like any other persistent property... + deleted column:"delFlag" + } + + static constraints = { + deleted nullable: true + } +} diff --git a/src/test/groovy/gorm/logical/delete/test/Person2TestData.groovy b/src/test/groovy/gorm/logical/delete/test/Person2TestData.groovy new file mode 100644 index 0000000..e24dce7 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person2TestData.groovy @@ -0,0 +1,32 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.PreQueryListener +import org.junit.After +import org.junit.Before + +trait Person2TestData { + + Closure doWithSpring() { + { -> + queryListener PreQueryListener + } + } + + @Before + void createUsers() { + try { + new Person2(userName: "Ben").save(failOnError: true, flush: true) + new Person2(userName: "Nirav").save(failOnError: true, flush: true) + new Person2(userName: "Jeff").save(failOnError: true, flush: true) + } catch (Exception e) { + println e + } + } + + @After + void cleanupUsers() { + Person2.withDeleted { + Person2.list()*.delete(hard: true) + } + } +} \ No newline at end of file diff --git a/src/test/groovy/gorm/logical/delete/test/Person3.groovy b/src/test/groovy/gorm/logical/delete/test/Person3.groovy new file mode 100644 index 0000000..9971eae --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person3.groovy @@ -0,0 +1,19 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.StringLogicalDelete +import grails.gorm.annotation.Entity + +@Entity +class Person3 implements StringLogicalDelete { + String userName + + static mapping = { + // the deleted property may be configured + // like any other persistent property... + deleted column:"delFlag" + } + + static constraints = { + deleted nullable: true + } +} diff --git a/src/test/groovy/gorm/logical/delete/test/Person3TestData.groovy b/src/test/groovy/gorm/logical/delete/test/Person3TestData.groovy new file mode 100644 index 0000000..7e5a7e6 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person3TestData.groovy @@ -0,0 +1,32 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.PreQueryListener +import org.junit.After +import org.junit.Before + +trait Person3TestData { + + Closure doWithSpring() { + { -> + queryListener PreQueryListener + } + } + + @Before + void createUsers() { + try { + new Person3(userName: "Ben").save(failOnError: true, flush: true) + new Person3(userName: "Nirav").save(failOnError: true, flush: true) + new Person3(userName: "Jeff").save(failOnError: true, flush: true) + } catch (Exception e) { + println e + } + } + + @After + void cleanupUsers() { + Person3.withDeleted { + Person3.list()*.delete(hard: true) + } + } +} From 86472b44ab50aab06160b8234d03fded6fc920d8 Mon Sep 17 00:00:00 2001 From: benrhine Date: Tue, 3 Apr 2018 11:32:04 -0400 Subject: [PATCH 2/9] logical delete for Object types Boolean, Date, String, with tests --- .../logical/delete/DateLogicalDelete.groovy | 103 ------- .../gorm/logical/delete/LogicalDelete.groovy | 2 +- .../logical/delete/PreQueryListener.groovy | 6 +- .../LogicalDeleteBase.groovy} | 51 ++-- .../typetrait/BooleanLogicalDelete.groovy | 36 +++ .../delete/typetrait/DateLogicalDelete.groovy | 38 +++ .../typetrait/StringLogicalDelete.groovy | 38 +++ .../logical/delete/BooleanCriteriaSpec.groovy | 62 +++++ .../delete/BooleanDetachedCriteriaSpec.groovy | 68 +++++ .../delete/BooleanDynamicFindersSpec.groovy | 91 +++++++ .../delete/BooleanLogicalDeleteSpec.groovy | 254 ++++++++++++++++++ .../delete/BooleanWithDeletedSpec.groovy | 112 ++++++++ .../gorm/logical/delete/test/Person2.groovy | 2 +- .../gorm/logical/delete/test/Person3.groovy | 2 +- .../gorm/logical/delete/test/Person4.groovy | 19 ++ .../delete/test/Person4TestData.groovy | 33 +++ 16 files changed, 774 insertions(+), 143 deletions(-) delete mode 100644 src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy rename src/main/groovy/gorm/logical/delete/{StringLogicalDelete.groovy => basetrait/LogicalDeleteBase.groovy} (68%) create mode 100644 src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy create mode 100644 src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy create mode 100644 src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy create mode 100644 src/test/groovy/gorm/logical/delete/BooleanCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/BooleanDetachedCriteriaSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/BooleanDynamicFindersSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/BooleanWithDeletedSpec.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person4.groovy create mode 100644 src/test/groovy/gorm/logical/delete/test/Person4TestData.groovy diff --git a/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy deleted file mode 100644 index 2ff4e48..0000000 --- a/src/main/groovy/gorm/logical/delete/DateLogicalDelete.groovy +++ /dev/null @@ -1,103 +0,0 @@ -package gorm.logical.delete - -import grails.gorm.DetachedCriteria -import org.grails.datastore.gorm.GormEnhancer -import org.grails.datastore.gorm.GormEntity -import org.grails.datastore.gorm.GormStaticApi - -import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER - -trait DateLogicalDelete extends GormEntity { - Date deleted = null - - static Object withDeleted(Closure closure) { - final initialThreadLocalValue = IGNORE_DELETED_FILTER.get() - try { - IGNORE_DELETED_FILTER.set(true) - return closure.call() - } finally { - IGNORE_DELETED_FILTER.set(initialThreadLocalValue) - } - } - - static D get(final Serializable id) { - if (IGNORE_DELETED_FILTER.get()) { - this.currentGormStaticApi().get(id) - } else { - new DetachedCriteria(this).build { - eq 'id', id - eq 'deleted', null - }.get() - } - } - - static D read(final Serializable id) { - if (IGNORE_DELETED_FILTER.get()) { - this.currentGormStaticApi().read(id) - } else { - new DetachedCriteria(this).build { - eq 'id', id - eq 'deleted', null - }.get() - } - } - - static D load(final Serializable id) { - if (IGNORE_DELETED_FILTER.get()) { - this.currentGormStaticApi().load(id) - } else { - new DetachedCriteria(this).build { - eq 'id', id - eq 'deleted', null - }.get() - } - } - - static D proxy(final Serializable id) { - if (IGNORE_DELETED_FILTER.get()) { - this.currentGormStaticApi().proxy(id) - } else { - new DetachedCriteria(this).build { - eq 'id', id - eq 'deleted', null - }.get() - } - } - - void delete() { - final Date date = new Date() - this.markDirty('deleted', date, null) - this.deleted = date - save() - } - - void delete(Map params) { - if (params?.hard) { - super.delete(params) - } else { - final Date date = new Date() - this.markDirty('deleted', date, null) - this.deleted = date - save(params) - } - } - - void unDelete() { - this.markDirty('deleted', null) - this.deleted = null - save() - } - - void unDelete(Map params) { - this.markDirty('deleted', null) - this.deleted = null - save(params) - } - - /** ============================================================================================ - * Private Methods: - * ============================================================================================= */ - private static GormStaticApi currentGormStaticApi() { - (GormStaticApi)GormEnhancer.findStaticApi(this) - } -} \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/LogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/LogicalDelete.groovy index 08d85b3..98389ad 100644 --- a/src/main/groovy/gorm/logical/delete/LogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/LogicalDelete.groovy @@ -25,7 +25,7 @@ import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER @CompileStatic trait LogicalDelete extends GormEntity { - Boolean deleted = false + boolean deleted = false static Object withDeleted(Closure closure) { final initialThreadLocalValue = IGNORE_DELETED_FILTER.get() diff --git a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy index c880fc9..0f9b95e 100644 --- a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy +++ b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy @@ -15,6 +15,7 @@ */ package gorm.logical.delete +import gorm.logical.delete.basetrait.LogicalDeleteBase import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.grails.datastore.mapping.model.PersistentEntity @@ -34,6 +35,7 @@ class PreQueryListener implements ApplicationListener { Query query = event.query PersistentEntity entity = query.entity + /** boolean primitive logical delete (false means not deleted) */ if (LogicalDelete.isAssignableFrom(entity.javaClass)) { log.debug "This entity [${entity.javaClass}] implements logical delete" @@ -42,7 +44,9 @@ class PreQueryListener implements ApplicationListener { } } - if (DateLogicalDelete.isAssignableFrom(entity.javaClass) || StringLogicalDelete.isAssignableFrom(entity.javaClass)) { + /** Date, String, Boolean logical delete (null means not deleted) */ + //if (DateLogicalDelete.isAssignableFrom(entity.javaClass) || StringLogicalDelete.isAssignableFrom(entity.javaClass) || BooleanLogicalDelete.isAssignableFrom(entity.javaClass)) { + if(LogicalDeleteBase.isAssignableFrom(entity.javaClass)) { log.debug "This entity [${entity.javaClass}] implements logical delete" if (!IGNORE_DELETED_FILTER.get()) { diff --git a/src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy similarity index 68% rename from src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy rename to src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy index 8022f83..03e2ed0 100644 --- a/src/main/groovy/gorm/logical/delete/StringLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy @@ -1,16 +1,23 @@ -package gorm.logical.delete +package gorm.logical.delete.basetrait import grails.gorm.DetachedCriteria import groovy.transform.CompileStatic import org.grails.datastore.gorm.GormEnhancer -import org.grails.datastore.gorm.GormEntity import org.grails.datastore.gorm.GormStaticApi import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER @CompileStatic -trait StringLogicalDelete extends GormEntity { - String deleted = null +trait LogicalDeleteBase { + static def deletedValue = null + + static void setDeletedValue(final newDeletedValue) { + deletedValue = newDeletedValue + } + + static returnDeletedValue() { + deletedValue + } static Object withDeleted(Closure closure) { final initialThreadLocalValue = IGNORE_DELETED_FILTER.get() @@ -28,7 +35,7 @@ trait StringLogicalDelete extends GormEntity { } else { new DetachedCriteria(this).build { eq 'id', id - eq 'deleted', null + eq 'deleted', deletedValue }.get() } } @@ -39,7 +46,7 @@ trait StringLogicalDelete extends GormEntity { } else { new DetachedCriteria(this).build { eq 'id', id - eq 'deleted', null + eq 'deleted', deletedValue }.get() } } @@ -50,7 +57,7 @@ trait StringLogicalDelete extends GormEntity { } else { new DetachedCriteria(this).build { eq 'id', id - eq 'deleted', null + eq 'deleted', deletedValue }.get() } } @@ -61,39 +68,11 @@ trait StringLogicalDelete extends GormEntity { } else { new DetachedCriteria(this).build { eq 'id', id - eq 'deleted', null + eq 'deleted', deletedValue }.get() } } - void delete(String newValue) { - this.markDirty('deleted', newValue, null) - this.deleted = newValue - save() - } - - void delete(Map params) { - if (params?.hard) { - super.delete(params) - } else { - this.markDirty('deleted', params?.newValue, null) - this.deleted = (String) params?.newValue - save(params) - } - } - - void unDelete() { - this.markDirty('deleted', null,) - this.deleted = null - save() - } - - void unDelete(Map params) { - this.markDirty('deleted', null) - this.deleted = null - save(params) - } - /** ============================================================================================ * Private Methods: * ============================================================================================= */ diff --git a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy new file mode 100644 index 0000000..3ccb701 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy @@ -0,0 +1,36 @@ +package gorm.logical.delete.typetrait + +import gorm.logical.delete.basetrait.LogicalDeleteBase +import org.grails.datastore.gorm.GormEntity + +trait BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { + Boolean deleted = null + + void delete() { + this.markDirty('deleted', true, null) + this.deleted = true + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', true, null) + this.deleted = true + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null, true) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', null, true) + this.deleted = null + save(params) + } +} \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy new file mode 100644 index 0000000..f3b07e5 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy @@ -0,0 +1,38 @@ +package gorm.logical.delete.typetrait + +import gorm.logical.delete.basetrait.LogicalDeleteBase +import org.grails.datastore.gorm.GormEntity + +trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { + Date deleted = null + + void delete() { + final Date date = new Date() + this.markDirty('deleted', date, null) + this.deleted = date + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + final Date date = new Date() + this.markDirty('deleted', date, null) + this.deleted = date + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', null) + this.deleted = null + save(params) + } +} \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy new file mode 100644 index 0000000..fefde6a --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy @@ -0,0 +1,38 @@ +package gorm.logical.delete.typetrait + +import gorm.logical.delete.basetrait.LogicalDeleteBase +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.GormEntity + +@CompileStatic +trait StringLogicalDelete implements GormEntity, LogicalDeleteBase { + String deleted = null + + void delete(String newValue) { + this.markDirty('deleted', newValue, null) + this.deleted = newValue + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', params?.newValue, null) + this.deleted = (String) params?.newValue + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null,) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', null) + this.deleted = null + save(params) + } +} diff --git a/src/test/groovy/gorm/logical/delete/BooleanCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanCriteriaSpec.groovy new file mode 100644 index 0000000..988b613 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/BooleanCriteriaSpec.groovy @@ -0,0 +1,62 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person4 +import gorm.logical.delete.test.Person4TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class BooleanCriteriaSpec extends Specification implements DomainUnitTest, Person4TestData { + + /******************* test criteria ***********************************/ + + @Rollback + void 'test criteria - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + // tag::criteria_query[] + def criteria = Person4.createCriteria() + def results = criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + // end::criteria_query[] + + then: "we should not get anything bc they were deleted" + !results + + when: + results = criteria { + eq("userName", "Jeff") + } + + then: + results + results[0].userName == 'Jeff' + } + + /******************* test criteria with projection ***********************************/ + + @Rollback + void 'test criteria with projection - logical deleted items'() { + // projection Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + def criteria = Person4.createCriteria() + def results = criteria.get { + projections { + count() + } + } + + then: "we should not get the deleted items" + results == 1 + } +} diff --git a/src/test/groovy/gorm/logical/delete/BooleanDetachedCriteriaSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanDetachedCriteriaSpec.groovy new file mode 100644 index 0000000..903f21b --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/BooleanDetachedCriteriaSpec.groovy @@ -0,0 +1,68 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person4 +import gorm.logical.delete.test.Person4TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class BooleanDetachedCriteriaSpec extends Specification implements DomainUnitTest, Person4TestData { + + /******************* test where ***********************************/ + + @Rollback + void 'test detached criteria where - logical deleted items'() { + // where detachedCriteria Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + // tag::detachedCriteria_query[] + DetachedCriteria query = Person4.where { + userName == "Ben" || userName == "Nirav" + } + def results = query.list() + // end::detachedCriteria_query[] + then: "we should not get anything bc they were deleted" + !results + + when: + query = Person4.where { + userName == "Jeff" + } + results = query.find() + + then: + results + results.userName == 'Jeff' + } + + /******************* test findall ***********************************/ + + @Rollback + void 'test detached criteria findAll - logical deleted items'() { + // findAll detachedCriteria Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + def results = Person4.findAll { + userName == "Ben" || userName == "Nirav" + } + + then: "we should not get anything bc they were deleted" + !results + + when: + results = Person4.findAll { + userName == "Jeff" + } + + then: + results + results[0].userName == 'Jeff' + } + + /********************* setup *****************************/ +} diff --git a/src/test/groovy/gorm/logical/delete/BooleanDynamicFindersSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanDynamicFindersSpec.groovy new file mode 100644 index 0000000..aa9ec7d --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/BooleanDynamicFindersSpec.groovy @@ -0,0 +1,91 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person4 +import gorm.logical.delete.test.Person4TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class BooleanDynamicFindersSpec extends Specification implements DomainUnitTest, Person4TestData { + + /******************* test FindAll ***********************************/ + + @Rollback + void 'test dynamic findAll hide logical deleted items'() { + // findAll() Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + List results = Person4.findAll() + + then: "we should only get those not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + + // list() calll + when: + results.clear() + results = Person4.list() + + then: + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test findBy ***************************/ + + @Rollback + void 'test dynamic findByUserName hide logical deleted items'() { + // findByUserName() Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + Person4 result1 = Person4.findByUserName("Ben") + Person4 result2 = Person4.findByUserName("Nirav") + + then: "we shouldn't get any bc it was deleted" + !result1 + !result2 + } + + /***************** test findByDeleted ***************************/ + + @Rollback + void 'test dynamic findByDeleted hide logical deleted items'() { + // findByDeleted() Call + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + List results = Person4.findAllByDeletedIsNotNull() + + then: "we should not get any because these are logically deleted" + results.size() == 0 + results.clear() + + when: + results = Person4.findAllByDeleted(null) + + then: "we should find the entity because it is not logically deleted" + results.size() == 1 + results[0].userName == 'Jeff' + } + + /***************** test get() ***************************/ + + @Rollback + void 'test dynamic get() finds logical deleted items'() { + when: "when 'get()' is used, we cannot access logically deleted entities" + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + final Person4 ben = Person4.get(1) + final Person4 nirav = Person4.get(2) + + then: + !nirav + !ben + } +} diff --git a/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy new file mode 100644 index 0000000..26a4da4 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy @@ -0,0 +1,254 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person4 +import gorm.logical.delete.test.Person4TestData +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class BooleanLogicalDeleteSpec extends Specification implements DomainUnitTest, Person4TestData { + + /******************* delete tests - (w/ get) ***********************************/ + + @Rollback + void 'test logical delete flush - get'() { + when: + Person4 p = (Person4) Person4.get(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person4.withDeleted { Person4.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - get'() { + when: + Person4 p = Person4.get(1) + + then: + !p.deleted + + when: + p.delete() + p = Person4.withDeleted { Person4.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - get'() { + when: + Person4 p = Person4.get(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person4.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ load) ***********************************/ + + @Rollback + void 'test logical delete flush - load'() { + when: + Person4 p = Person4.load(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person4.withDeleted { Person4.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - load'() { + when: + Person4 p = Person4.load(1) + + then: + !p.deleted + + when: + p.delete() + p = Person4.withDeleted { Person4.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - load'() { + when: + Person4 p = Person4.load(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person4.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ proxy) ***********************************/ + + @Rollback + void 'test logical delete flush - proxy'() { + when: + Person4 p = Person4.proxy(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person4.withDeleted { Person4.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - proxy'() { + when: + Person4 p = Person4.proxy(1) + + then: + !p.deleted + + when: + p.delete() + p = Person4.withDeleted { Person4.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - proxy'() { + when: + Person4 p = Person4.proxy(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person4.count() == 2 // 2 left after one hard deleted + } + + /******************* delete tests - (w/ read) ***********************************/ + + @Rollback + void 'test logical delete flush - read'() { + when: + Person4 p = Person4.read(1) + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + p = Person4.withDeleted { Person4.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - read'() { + when: + Person4 p = Person4.read(1) + + then: + !p.deleted + + when: + p.delete() + p = Person4.withDeleted { Person4.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical hard delete - read'() { + when: + Person4 p = Person4.read(1) + + then: + !p.deleted + + when: + p.delete(hard: true) + p.discard() + + then: + Person4.count() == 2 // 2 left after one hard deleted + } + + /******************* undelete tests ***********************************/ + + @Rollback + void 'test logical unDelete flush'() { + when: + Person4 p = Person4.get(1) + p.delete() + p = Person4.withDeleted { Person4.get(1) } + + then: + p.deleted + + when: + p.unDelete(flush: true) + p = Person4.get(1) + + then: + !p.deleted + + } + + @Rollback + void 'test logical unDelete'() { + when: + Person4 p = Person4.get(1) + p.delete() + p = Person4.withDeleted { Person4.get(1) } + + then: + p.deleted + + when: + p.unDelete() + p = Person4.get(1) + + then: + !p.deleted + + } +} diff --git a/src/test/groovy/gorm/logical/delete/BooleanWithDeletedSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanWithDeletedSpec.groovy new file mode 100644 index 0000000..8d59ae5 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/BooleanWithDeletedSpec.groovy @@ -0,0 +1,112 @@ +package gorm.logical.delete + +import gorm.logical.delete.test.Person4 +import gorm.logical.delete.test.Person4TestData +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class BooleanWithDeletedSpec extends Specification implements DomainUnitTest, Person4TestData { + + /******************* test with delete ***********************************/ + + @Rollback + void 'test withDeleted findAll - logical deleted items'() { + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + def results = Person4.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + + // tag::find_all_with_deleted[] + results = Person4.withDeleted { Person4.findAll() } + // end::find_all_with_deleted[] + + then: + results.size() == 3 + } + + @Rollback + void 'test withDeleted detached criteria'() { + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + DetachedCriteria query = Person4.where { + userName == "Ben" || userName == "Nirav" + } + def results = Person4.withDeleted { + query.list() + } + + then: "we should get deleted items" + results.size() == 2 + } + + @Rollback + void 'test withDeleted criteria'() { + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + def criteria = Person4.createCriteria() + def results = Person4.withDeleted { + criteria { + or { + eq("userName", "Ben") + eq("userName", "Nirav") + } + } + } + + then: "we should get deleted items" + results.size() == 2 + + } + + void 'test that the thread local is restored to false even if the closure throws an exception'() { + when: + Person4.withDeleted { + throw new IllegalStateException() + } + + then: + thrown IllegalStateException + + and: + !PreQueryListener.IGNORE_DELETED_FILTER.get() + } + + void 'test that nested .withDeleted calls work as expected'() { + // One wouldn't directly nest calls to withDeleted intentionally + // but a service method could use withDeleted and invoke another service + // method which also invokes with deleted, and that could cause a problem + when: + assert Person4.count() == 3 + Person4.findByUserName("Ben").delete() + Person4.findByUserName("Nirav").delete() + def results = Person4.findAll() + + then: "we should get only deleted=false items" + results.size() == 1 + + when: "We should get all items - included deleted" + results = Person4.withDeleted { + Person4.withDeleted {} + + // make sure the filter is still working after the previous call + // to withDeletedl... + Person4.findAll() + } + + then: + results.size() == 3 + + } +} diff --git a/src/test/groovy/gorm/logical/delete/test/Person2.groovy b/src/test/groovy/gorm/logical/delete/test/Person2.groovy index f65ea1c..c6e46ab 100644 --- a/src/test/groovy/gorm/logical/delete/test/Person2.groovy +++ b/src/test/groovy/gorm/logical/delete/test/Person2.groovy @@ -1,6 +1,6 @@ package gorm.logical.delete.test -import gorm.logical.delete.DateLogicalDelete +import gorm.logical.delete.typetrait.DateLogicalDelete import grails.gorm.annotation.Entity @Entity diff --git a/src/test/groovy/gorm/logical/delete/test/Person3.groovy b/src/test/groovy/gorm/logical/delete/test/Person3.groovy index 9971eae..47f8aa5 100644 --- a/src/test/groovy/gorm/logical/delete/test/Person3.groovy +++ b/src/test/groovy/gorm/logical/delete/test/Person3.groovy @@ -1,6 +1,6 @@ package gorm.logical.delete.test -import gorm.logical.delete.StringLogicalDelete +import gorm.logical.delete.typetrait.StringLogicalDelete import grails.gorm.annotation.Entity @Entity diff --git a/src/test/groovy/gorm/logical/delete/test/Person4.groovy b/src/test/groovy/gorm/logical/delete/test/Person4.groovy new file mode 100644 index 0000000..ff173ca --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person4.groovy @@ -0,0 +1,19 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.typetrait.BooleanLogicalDelete +import grails.gorm.annotation.Entity + +@Entity +class Person4 implements BooleanLogicalDelete { + String userName + + static mapping = { + // the deleted property may be configured + // like any other persistent property... + deleted column:"delFlag" + } + + static constraints = { + deleted nullable: true + } +} diff --git a/src/test/groovy/gorm/logical/delete/test/Person4TestData.groovy b/src/test/groovy/gorm/logical/delete/test/Person4TestData.groovy new file mode 100644 index 0000000..54f6097 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/test/Person4TestData.groovy @@ -0,0 +1,33 @@ +package gorm.logical.delete.test + +import gorm.logical.delete.PreQueryListener +import org.junit.After +import org.junit.Before + +trait Person4TestData { + + Closure doWithSpring() { + { -> + queryListener PreQueryListener + } + } + + @Before + void createUsers() { + try { + new Person4(userName: "Ben").save(failOnError: true, flush: true) + new Person4(userName: "Nirav").save(failOnError: true, flush: true) + new Person4(userName: "Jeff").save(failOnError: true, flush: true) + } catch (Exception e) { + println e + } + } + + @After + void cleanupUsers() { + Person4.withDeleted { + Person4.list()*.delete(hard: true) + } + } + +} \ No newline at end of file From d3be7e6f967e92eb64887492a04a3d60cdb4f42a Mon Sep 17 00:00:00 2001 From: benrhine Date: Tue, 3 Apr 2018 11:32:40 -0400 Subject: [PATCH 3/9] logical delete for Object types Boolean, Date, String, with tests --- src/main/groovy/gorm/logical/delete/PreQueryListener.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy index 0f9b95e..330d056 100644 --- a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy +++ b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy @@ -45,7 +45,6 @@ class PreQueryListener implements ApplicationListener { } /** Date, String, Boolean logical delete (null means not deleted) */ - //if (DateLogicalDelete.isAssignableFrom(entity.javaClass) || StringLogicalDelete.isAssignableFrom(entity.javaClass) || BooleanLogicalDelete.isAssignableFrom(entity.javaClass)) { if(LogicalDeleteBase.isAssignableFrom(entity.javaClass)) { log.debug "This entity [${entity.javaClass}] implements logical delete" From 8ccd7252819c8485e610b1a9a5306b449220ecb3 Mon Sep 17 00:00:00 2001 From: benrhine Date: Wed, 4 Apr 2018 12:44:51 -0400 Subject: [PATCH 4/9] added ability to pass in custom values for Boolean, Date, and String on delete --- .../typetrait/BooleanLogicalDelete.groovy | 16 +-- .../delete/typetrait/DateLogicalDelete.groovy | 15 +- .../typetrait/StringLogicalDelete.groovy | 14 +- .../delete/BooleanLogicalDeleteSpec.groovy | 2 +- .../delete/DateLogicalDeleteSpec.groovy | 132 ++++++++++++++++++ .../delete/StringLogicalDeleteSpec.groovy | 132 ++++++++++++++++++ 6 files changed, 287 insertions(+), 24 deletions(-) diff --git a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy index 3ccb701..faac6d6 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy @@ -6,9 +6,9 @@ import org.grails.datastore.gorm.GormEntity trait BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { Boolean deleted = null - void delete() { - this.markDirty('deleted', true, null) - this.deleted = true + void delete(Boolean newValue = Boolean.TRUE) { + this.markDirty('deleted', newValue, this.deleted) + this.deleted = newValue save() } @@ -16,21 +16,21 @@ trait BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { if (params?.hard) { super.delete(params) } else { - this.markDirty('deleted', true, null) - this.deleted = true + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Boolean) params?.newValue ?: Boolean.TRUE save(params) } } void unDelete() { - this.markDirty('deleted', null, true) + this.markDirty('deleted', null, this.deleted) this.deleted = null save() } void unDelete(Map params) { - this.markDirty('deleted', null, true) - this.deleted = null + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Boolean) params?.newValue ?: null save(params) } } \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy index f3b07e5..2d3a69a 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy @@ -6,9 +6,8 @@ import org.grails.datastore.gorm.GormEntity trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { Date deleted = null - void delete() { - final Date date = new Date() - this.markDirty('deleted', date, null) + void delete(Date date = new Date()) { + this.markDirty('deleted', date, this.deleted) this.deleted = date save() } @@ -18,21 +17,21 @@ trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { super.delete(params) } else { final Date date = new Date() - this.markDirty('deleted', date, null) - this.deleted = date + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Date) params?.newValue ?: new Date() save(params) } } void unDelete() { - this.markDirty('deleted', null) + this.markDirty('deleted', null, this.deleted) this.deleted = null save() } void unDelete(Map params) { - this.markDirty('deleted', null) - this.deleted = null + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Date) params?.newValue ?: null save(params) } } \ No newline at end of file diff --git a/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy index fefde6a..967206a 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/StringLogicalDelete.groovy @@ -8,8 +8,8 @@ import org.grails.datastore.gorm.GormEntity trait StringLogicalDelete implements GormEntity, LogicalDeleteBase { String deleted = null - void delete(String newValue) { - this.markDirty('deleted', newValue, null) + void delete(String newValue = 'deleted') { + this.markDirty('deleted', newValue, this.deleted) this.deleted = newValue save() } @@ -18,21 +18,21 @@ trait StringLogicalDelete implements GormEntity, LogicalDeleteBase { if (params?.hard) { super.delete(params) } else { - this.markDirty('deleted', params?.newValue, null) - this.deleted = (String) params?.newValue + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (String) params?.newValue ?: 'deleted' save(params) } } void unDelete() { - this.markDirty('deleted', null,) + this.markDirty('deleted', null, this.deleted) this.deleted = null save() } void unDelete(Map params) { - this.markDirty('deleted', null) - this.deleted = null + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (String) params?.newValue ?: null save(params) } } diff --git a/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy b/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy index 26a4da4..c44cede 100644 --- a/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy +++ b/src/test/groovy/gorm/logical/delete/BooleanLogicalDeleteSpec.groovy @@ -13,7 +13,7 @@ class BooleanLogicalDeleteSpec extends Specification implements DomainUnitTest

Date: Wed, 4 Apr 2018 12:49:04 -0400 Subject: [PATCH 5/9] adding missing compilestatic --- .../gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy | 2 ++ .../gorm/logical/delete/typetrait/DateLogicalDelete.groovy | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy index faac6d6..e4ca98a 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy @@ -1,8 +1,10 @@ package gorm.logical.delete.typetrait import gorm.logical.delete.basetrait.LogicalDeleteBase +import groovy.transform.CompileStatic import org.grails.datastore.gorm.GormEntity +@CompileStatic trait BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { Boolean deleted = null diff --git a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy index 2d3a69a..149212d 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy @@ -1,8 +1,10 @@ package gorm.logical.delete.typetrait import gorm.logical.delete.basetrait.LogicalDeleteBase +import groovy.transform.CompileStatic import org.grails.datastore.gorm.GormEntity +@CompileStatic trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { Date deleted = null From f1c7643e22ab62df98cb3180661189107d6504c6 Mon Sep 17 00:00:00 2001 From: benrhine Date: Fri, 6 Apr 2018 12:28:02 -0400 Subject: [PATCH 6/9] test commit --- src/test/groovy/gorm/logical/delete/test/Person4.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/groovy/gorm/logical/delete/test/Person4.groovy b/src/test/groovy/gorm/logical/delete/test/Person4.groovy index ff173ca..eccc540 100644 --- a/src/test/groovy/gorm/logical/delete/test/Person4.groovy +++ b/src/test/groovy/gorm/logical/delete/test/Person4.groovy @@ -17,3 +17,5 @@ class Person4 implements BooleanLogicalDelete { deleted nullable: true } } + +//test comment From 363c6320ad431835281dedd7680becf47c2ea1a0 Mon Sep 17 00:00:00 2001 From: benrhine Date: Fri, 6 Apr 2018 12:28:52 -0400 Subject: [PATCH 7/9] undoing test commit --- src/test/groovy/gorm/logical/delete/test/Person4.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/groovy/gorm/logical/delete/test/Person4.groovy b/src/test/groovy/gorm/logical/delete/test/Person4.groovy index eccc540..7d7fa52 100644 --- a/src/test/groovy/gorm/logical/delete/test/Person4.groovy +++ b/src/test/groovy/gorm/logical/delete/test/Person4.groovy @@ -16,6 +16,4 @@ class Person4 implements BooleanLogicalDelete { static constraints = { deleted nullable: true } -} - -//test comment +} \ No newline at end of file From 2bbc93088b6e3737b1cdbbbae9169d74851492d5 Mon Sep 17 00:00:00 2001 From: Matthew Moss Date: Wed, 8 Dec 2021 09:01:11 -0600 Subject: [PATCH 8/9] Use isNull rather than eq when checking for null Query.eq with a null value does not do as expected; use the more specific Query.isNull restriction. --- src/main/groovy/gorm/logical/delete/PreQueryListener.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy index 330d056..5188937 100644 --- a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy +++ b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy @@ -49,7 +49,7 @@ class PreQueryListener implements ApplicationListener { log.debug "This entity [${entity.javaClass}] implements logical delete" if (!IGNORE_DELETED_FILTER.get()) { - query.eq('deleted', null) + query.isNull('deleted') } } } catch (Exception e) { From 84ab254afd825db96208989780270e1f89acb782 Mon Sep 17 00:00:00 2001 From: Matthew Moss Date: Wed, 8 Dec 2021 12:52:41 -0600 Subject: [PATCH 9/9] Fix codenarc violations --- src/main/groovy/gorm/logical/delete/PreQueryListener.groovy | 2 +- .../gorm/logical/delete/basetrait/LogicalDeleteBase.groovy | 4 ++-- .../gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy | 2 +- .../gorm/logical/delete/typetrait/DateLogicalDelete.groovy | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy index 5188937..b544f74 100644 --- a/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy +++ b/src/main/groovy/gorm/logical/delete/PreQueryListener.groovy @@ -45,7 +45,7 @@ class PreQueryListener implements ApplicationListener { } /** Date, String, Boolean logical delete (null means not deleted) */ - if(LogicalDeleteBase.isAssignableFrom(entity.javaClass)) { + if (LogicalDeleteBase.isAssignableFrom(entity.javaClass)) { log.debug "This entity [${entity.javaClass}] implements logical delete" if (!IGNORE_DELETED_FILTER.get()) { diff --git a/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy index 03e2ed0..631ef64 100644 --- a/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy +++ b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy @@ -9,7 +9,7 @@ import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER @CompileStatic trait LogicalDeleteBase { - static def deletedValue = null + static deletedValue = null static void setDeletedValue(final newDeletedValue) { deletedValue = newDeletedValue @@ -77,6 +77,6 @@ trait LogicalDeleteBase { * Private Methods: * ============================================================================================= */ private static GormStaticApi currentGormStaticApi() { - (GormStaticApi)GormEnhancer.findStaticApi(this) + (GormStaticApi) GormEnhancer.findStaticApi(this) } } diff --git a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy index e4ca98a..c294ca3 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.groovy @@ -35,4 +35,4 @@ trait BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { this.deleted = (Boolean) params?.newValue ?: null save(params) } -} \ No newline at end of file +} diff --git a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy index 149212d..0a4cf4f 100644 --- a/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy +++ b/src/main/groovy/gorm/logical/delete/typetrait/DateLogicalDelete.groovy @@ -18,7 +18,6 @@ trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { if (params?.hard) { super.delete(params) } else { - final Date date = new Date() this.markDirty('deleted', params?.newValue, this.deleted) this.deleted = (Date) params?.newValue ?: new Date() save(params) @@ -36,4 +35,4 @@ trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { this.deleted = (Date) params?.newValue ?: null save(params) } -} \ No newline at end of file +}