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 914793c..b544f74 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" @@ -41,6 +43,15 @@ class PreQueryListener implements ApplicationListener { query.eq('deleted', false) } } + + /** Date, String, Boolean logical delete (null means not deleted) */ + if (LogicalDeleteBase.isAssignableFrom(entity.javaClass)) { + log.debug "This entity [${entity.javaClass}] implements logical delete" + + if (!IGNORE_DELETED_FILTER.get()) { + query.isNull('deleted') + } + } } catch (Exception e) { log.error(e.message) } diff --git a/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy new file mode 100644 index 0000000..631ef64 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/basetrait/LogicalDeleteBase.groovy @@ -0,0 +1,82 @@ +package gorm.logical.delete.basetrait + +import grails.gorm.DetachedCriteria +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormStaticApi + +import static gorm.logical.delete.PreQueryListener.IGNORE_DELETED_FILTER + +@CompileStatic +trait LogicalDeleteBase { + static deletedValue = null + + static void setDeletedValue(final newDeletedValue) { + deletedValue = newDeletedValue + } + + static returnDeletedValue() { + deletedValue + } + + 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', deletedValue + }.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', deletedValue + }.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', deletedValue + }.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', deletedValue + }.get() + } + } + + /** ============================================================================================ + * Private Methods: + * ============================================================================================= */ + private static GormStaticApi currentGormStaticApi() { + (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 new file mode 100644 index 0000000..c294ca3 --- /dev/null +++ b/src/main/groovy/gorm/logical/delete/typetrait/BooleanLogicalDelete.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 BooleanLogicalDelete implements GormEntity, LogicalDeleteBase { + Boolean deleted = null + + void delete(Boolean newValue = Boolean.TRUE) { + this.markDirty('deleted', newValue, this.deleted) + this.deleted = newValue + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Boolean) params?.newValue ?: Boolean.TRUE + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null, this.deleted) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Boolean) params?.newValue ?: null + save(params) + } +} 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..0a4cf4f --- /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 groovy.transform.CompileStatic +import org.grails.datastore.gorm.GormEntity + +@CompileStatic +trait DateLogicalDelete implements GormEntity, LogicalDeleteBase { + Date deleted = null + + void delete(Date date = new Date()) { + this.markDirty('deleted', date, this.deleted) + this.deleted = date + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Date) params?.newValue ?: new Date() + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null, this.deleted) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (Date) params?.newValue ?: null + save(params) + } +} 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..967206a --- /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 = 'deleted') { + this.markDirty('deleted', newValue, this.deleted) + this.deleted = newValue + save() + } + + void delete(Map params) { + if (params?.hard) { + super.delete(params) + } else { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (String) params?.newValue ?: 'deleted' + save(params) + } + } + + void unDelete() { + this.markDirty('deleted', null, this.deleted) + this.deleted = null + save() + } + + void unDelete(Map params) { + this.markDirty('deleted', params?.newValue, this.deleted) + this.deleted = (String) params?.newValue ?: 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..c44cede --- /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.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/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..c53a6c0 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/DateLogicalDeleteSpec.groovy @@ -0,0 +1,386 @@ +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(newValue: new Date(), flush:true) + p.discard() + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete flush - get with default'() { + 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(new Date()) + p = Person2.withDeleted { Person2.get(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - get with default'() { + 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(newValue: new Date(), flush:true) + p.discard() + p = Person2.withDeleted { Person2.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete flush - load with default'() { + 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(new Date()) + p = Person2.withDeleted { Person2.load(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - load with default'() { + 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(newValue: new Date(), flush:true) + p.discard() + p = Person2.withDeleted { Person2.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete flush - proxy with default'() { + 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(new Date()) + p = Person2.withDeleted { Person2.proxy(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - proxy with default'() { + 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(newValue: new Date(), flush:true) + p.discard() + p = Person2.withDeleted { Person2.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete flush - read with default'() { + 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(new Date()) + p = Person2.withDeleted { Person2.read(1) } + + then: + p.deleted + } + + @Rollback + void 'test logical delete - read with default'() { + 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..199c675 --- /dev/null +++ b/src/test/groovy/gorm/logical/delete/StringLogicalDeleteSpec.groovy @@ -0,0 +1,386 @@ +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 flush - get when default value is used'() { + when: + Person3 p = Person3.get(1) + + then: + !p.deleted + + when: + p.delete(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 delete - get when default value is used'() { + when: + Person3 p = Person3.get(1) + + then: + !p.deleted + + when: + p.delete() + 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 flush - load when default is used'() { + when: + Person3 p = Person3.load(1) + + then: + !p.deleted + + when: + p.delete(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 delete - load when default is used'() { + when: + Person3 p = Person3.load(1) + + then: + !p.deleted + + when: + p.delete() + 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 flush - proxy when default is used'() { + when: + Person3 p = Person3.proxy(1) + + then: + !p.deleted + + when: + p.delete(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 delete - proxy when default is used'() { + when: + Person3 p = Person3.proxy(1) + + then: + !p.deleted + + when: + p.delete() + 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 flush - read when default is used'() { + when: + Person3 p = Person3.read(1) + + then: + !p.deleted + + when: + p.delete(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 delete - read when default is used'() { + when: + Person3 p = Person3.read(1) + + then: + !p.deleted + + when: + p.delete() + 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..c6e46ab --- /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.typetrait.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..47f8aa5 --- /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.typetrait.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) + } + } +} 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..7d7fa52 --- /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 + } +} \ No newline at end of file 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