This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch merge-hibernate6 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 83be4126d9c0b81df0572e9cdc49b19007db1df6 Author: Walter Duque de Estrada <wbdu...@mac.com> AuthorDate: Thu Aug 7 21:36:32 2025 -0500 Size queries --- .../hibernate/query/AbstractHibernateQuery.java | 47 +-- .../orm/hibernate/query/PredicateGenerator.java | 40 ++- .../CascadeBehaviorFetcherSpec.groovy | 8 + .../testing/tck/domains/Child_BT_Default_P.groovy | 9 + .../testing/tck/domains/Owner_Default_Bi_P.groovy | 10 + .../data/testing/tck/tests/SizeQuerySpec.groovy | 373 +++++++-------------- 6 files changed, 180 insertions(+), 307 deletions(-) diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java index 64777e5d6b..c5db1527d5 100644 --- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java @@ -42,6 +42,8 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaExpression; import org.hibernate.transform.ResultTransformer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -63,9 +65,9 @@ import java.util.function.Predicate; * @since 1.0 */ @SuppressWarnings("rawtypes") -@Slf4j +//@Slf4j public abstract class AbstractHibernateQuery extends Query { - + private static final Logger LOG = LoggerFactory.getLogger(AbstractHibernateQuery.class); public static final String SIZE_CONSTRAINT_PREFIX = "Size"; protected static final String ALIAS = "_alias"; @@ -166,6 +168,10 @@ public abstract class AbstractHibernateQuery extends Query { detachedCriteria.add(criterion); } + public void add(DetachedCriteria<?> detachedCriteria) { + detachedCriteria.add(new Conjunction(detachedCriteria.getCriteria())); + } + /** * This is called for ORS and is only used by DymamicFinder @@ -196,11 +202,7 @@ public abstract class AbstractHibernateQuery extends Query { @Override public Query gt(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.gt(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } + detachedCriteria.gt(property, value); return this; } @@ -276,52 +278,31 @@ public abstract class AbstractHibernateQuery extends Query { @Override public Query ge(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.ge(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } + detachedCriteria.ge(property, value); return this; } @Override public Query le(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.le(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } + detachedCriteria.le(property, value); return this; } @Override public Query gte(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.gte(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } - + detachedCriteria.gte(property, value); return this; } @Override public Query lte(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.lte(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } + detachedCriteria.lte(property, value); return this; } @Override public Query lt(String property, Object value) { - if(value instanceof Number number) { - detachedCriteria.lt(property, number); - } else{ - throw new ConfigurationException("gte only uses numbers"); - } + detachedCriteria.lt(property, value); return this; } diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java index 837dea451a..14380d6c32 100644 --- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java @@ -11,17 +11,15 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; +import org.grails.datastore.mapping.core.exceptions.ConfigurationException; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.query.Query; import org.grails.datastore.mapping.query.api.QueryableCriteria; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaInPredicate; -import org.hibernate.query.criteria.JpaJoin; -import org.hibernate.query.criteria.JpaPath; import org.hibernate.query.criteria.JpaPredicate; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.domain.SqmSetJoin; import org.hibernate.query.sqm.tree.predicate.SqmInListPredicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,25 +102,25 @@ public class PredicateGenerator { } else if (criterion instanceof Query.IdEquals c) { return cb.equal(root_.get("id"), c.getValue()); } else if (criterion instanceof Query.GreaterThan c) { - return cb.gt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), (Number) c.getValue()); + return cb.gt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), getNumericValue(c)); } else if (criterion instanceof Query.GreaterThanEquals c) { - return cb.ge(fromsByProvider.getFullyQualifiedPath(c.getProperty()), (Number) c.getValue()); + return cb.ge(fromsByProvider.getFullyQualifiedPath(c.getProperty()), getNumericValue(c)); } else if (criterion instanceof Query.LessThan c) { - return cb.lt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), (Number) c.getValue()); + return cb.lt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), getNumericValue(c)); } else if (criterion instanceof Query.LessThanEquals c) { - return cb.le(fromsByProvider.getFullyQualifiedPath(c.getProperty()), (Number) c.getValue()); + return cb.le(fromsByProvider.getFullyQualifiedPath(c.getProperty()), getNumericValue(c)); } else if (criterion instanceof Query.SizeEquals c) { return cb.equal(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), c.getValue()); } else if (criterion instanceof Query.SizeNotEquals c) { return cb.notEqual(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), c.getValue()); } else if (criterion instanceof Query.SizeGreaterThan c) { - return cb.gt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.gt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeGreaterThanEquals c) { - return cb.ge(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.ge(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeLessThan c) { - return cb.lt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.lt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeLessThanEquals c) { - return cb.le(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.le(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.Between c) { if (c.getFrom() instanceof String && c.getTo() instanceof String) { return cb.between(fromsByProvider.getFullyQualifiedPath(c.getProperty()), (String) c.getFrom(), (String) c.getTo()); @@ -154,13 +152,13 @@ public class PredicateGenerator { } else if (criterion instanceof Query.SizeEquals c) { return cb.equal(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), c.getValue()); } else if (criterion instanceof Query.SizeGreaterThan c) { - return cb.gt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.gt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeGreaterThanEquals c) { - return cb.ge(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.ge(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeLessThan c) { - return cb.lt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.lt(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if (criterion instanceof Query.SizeLessThanEquals c) { - return cb.le(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), (Number) c.getValue()); + return cb.le(cb.size(fromsByProvider.getFullyQualifiedPath(c.getProperty())), getNumericValue(c)); } else if(criterion instanceof Query.In || criterion instanceof Query.NotIn) { var queryableCriteria = getQueryableCriteriaFromInCriteria(criterion); if (Objects.nonNull(queryableCriteria)) { @@ -332,4 +330,16 @@ public class PredicateGenerator { .map(expressible -> expressible.getExpressibleJavaType().getJavaTypeClass()) .orElse(null); } + + private static Number getNumericValue(Query.PropertyCriterion criterion) { + Object value = criterion.getValue(); + if (value instanceof Number) { + return (Number) value; + } + String operationName = criterion.getClass().getSimpleName(); + throw new ConfigurationException( + String.format("Operation '%s' on property '%s' only accepts a numeric value, but received a %s", + operationName, criterion.getProperty(), (value == null ? "null" : value.getClass().getName()))); + } + } diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy index 8d141d1b0b..11f865c48a 100644 --- a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy +++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy @@ -42,6 +42,7 @@ class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { ["many-to-many (inverse side)" , Tag_BT , "posts" , Post , NONE.getValue()], ["many-to-many (circular superclass)", Mammal, "dogs", Dog, NONE.getValue()], ["many-to-one (belongsTo)" , Book_BT_Default , "author" , AW_Default_Bi , NONE.getValue()], + ["many-to-one (unidirectional)" , A , "manyToOne", ManyToOne , SAVE_UPDATE.getValue()], ["many-to-one (bidirectional but superclass)" , Bird , "canary" , Canary , NONE.getValue()], // --- Additional Hibernate 6+ specific scenarios --- @@ -141,6 +142,13 @@ class Buffalo{} @Entity class Book_BT_Default { String title; static belongsTo = [author: AW_Default_Bi] } @Entity class AW_Default_Bi { static hasMany = [books: Book_BT_Default] } +@Entity +class A { + ManyToOne manyToOne +} +@Entity +class ManyToOne { +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Child_BT_Default_P.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Child_BT_Default_P.groovy new file mode 100644 index 0000000000..c36559db11 --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Child_BT_Default_P.groovy @@ -0,0 +1,9 @@ +package org.apache.grails.data.testing.tck.domains + +import grails.gorm.annotation.Entity + +@Entity +class Child_BT_Default_P { + String title + static belongsTo = [owner: Owner_Default_Bi_P] +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Owner_Default_Bi_P.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Owner_Default_Bi_P.groovy new file mode 100644 index 0000000000..962d7d788c --- /dev/null +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/domains/Owner_Default_Bi_P.groovy @@ -0,0 +1,10 @@ +package org.apache.grails.data.testing.tck.domains + +import grails.gorm.annotation.Entity + +@Entity +class Owner_Default_Bi_P { + String name + Set<Child_BT_Default_P> children + static hasMany = [children: Child_BT_Default_P] +} diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/SizeQuerySpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/SizeQuerySpec.groovy index 7e724ca50a..0524e5b6cd 100644 --- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/SizeQuerySpec.groovy +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/SizeQuerySpec.groovy @@ -18,317 +18,172 @@ */ package org.apache.grails.data.testing.tck.tests -import org.apache.grails.data.testing.tck.domains.SimpleCountry -import org.apache.grails.data.testing.tck.domains.Person import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.apache.grails.data.testing.tck.domains.Child_BT_Default_P +import org.apache.grails.data.testing.tck.domains.Owner_Default_Bi_P +import spock.lang.Unroll /** * Tests for querying the size of collections etc. */ class SizeQuerySpec extends GrailsDataTckSpec { void setupSpec() { - manager.addAllDomainClasses([SimpleCountry, Person]) + manager.addAllDomainClasses([Owner_Default_Bi_P, Child_BT_Default_P]) } - - void "Test sizeLe criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) + private void setupTestData() { + // Owner A has 1 child + new Owner_Default_Bi_P(name: "Owner A") + .addToChildren(new Child_BT_Default_P(title: "Child 1")) .save(flush: true) - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") + // Owner B has 2 children + new Owner_Default_Bi_P(name: "Owner B") + .addToChildren(new Child_BT_Default_P(title: "Child 5")) + .addToChildren(new Child_BT_Default_P(title: "Child 6")) .save(flush: true) - new SimpleCountry(name: "Miami") - .addToResidents(firstName: "Dexter", lastName: "Morgan") - .addToResidents(firstName: "Debra", lastName: "Morgan") + // Owner C has 3 children + new Owner_Default_Bi_P(name: "Owner C") + .addToChildren(new Child_BT_Default_P(title: "Child 2")) + .addToChildren(new Child_BT_Default_P(title: "Child 3")) + .addToChildren(new Child_BT_Default_P(title: "Child 4")) .save(flush: true) manager.session.clear() + } - when: "We query for countries with 1 resident" - def results = SimpleCountry.withCriteria { - sizeLe "residents", 3 - order "name" - } - - then: "We get the correct result back" - results != null - results.size() == 3 - results[0].name == 'Dinoville' - results[1].name == 'Miami' - results[2].name == 'Springfield' + @Unroll("Test sizeLe criterion with size #size expects #expectedNames") + void "Test sizeLe criterion"(int size, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() - when: "We query for countries with 2 resident" - results = SimpleCountry.withCriteria { - sizeLe "residents", 2 + when: "We query for owners with at most #size children" + def results = Owner_Default_Bi_P.withCriteria { + sizeLe "children", size order "name" } - then: "We get the correct result back" - results != null - results.size() == 2 - results[0].name == 'Dinoville' - results[1].name == 'Miami' + then: "We get the correct owners back" + results*.name == expectedNames - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - sizeLe "residents", 1 - } - - then: "we get 1 result back" - results.size() == 1 + where: + size | expectedNames + 3 | ['Owner A', 'Owner B', 'Owner C'] + 2 | ['Owner A', 'Owner B'] + 1 | ['Owner A'] + 0 | [] } - void "Test sizeLt criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) - .save(flush: true) + @Unroll("Test sizeLt criterion with size #size expects #expectedNames") + void "Test sizeLt criterion"(int size, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") - .save(flush: true) - - new SimpleCountry(name: "Miami") - .addToResidents(firstName: "Dexter", lastName: "Morgan") - .addToResidents(firstName: "Debra", lastName: "Morgan") - .save(flush: true) - - manager.session.clear() - - when: "We query for countries with 1 resident" - def results = SimpleCountry.withCriteria { - sizeLt "residents", 3 + when: "We query for owners with less than #size children" + def results = Owner_Default_Bi_P.withCriteria { + sizeLt "children", size order "name" } - then: "We get the correct result back" - results != null - results.size() == 2 - results[0].name == 'Dinoville' - results[1].name == 'Miami' - - when: "We query for countries with 2 resident" - results = SimpleCountry.withCriteria { - sizeLt "residents", 2 - } - - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Dinoville' + then: "We get the correct owners back" + results*.name == expectedNames - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - sizeLt "residents", 1 - } - - then: "we get no results back" - results.size() == 0 + where: + size | expectedNames + 3 | ['Owner A', 'Owner B'] + 2 | ['Owner A'] + 1 | [] } - void "Test sizeGt criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) - .save(flush: true) - - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") - .save(flush: true) - - new SimpleCountry(name: "Miami") - .addToResidents(firstName: "Dexter", lastName: "Morgan") - .addToResidents(firstName: "Debra", lastName: "Morgan") - .save(flush: true) - - manager.session.clear() + @Unroll("Test sizeGt criterion with size #size expects #expectedNames") + void "Test sizeGt criterion"(int size, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() - when: "We query for countries with 1 resident" - def results = SimpleCountry.withCriteria { - sizeGt "residents", 1 + when: "We query for owners with more than #size children" + def results = Owner_Default_Bi_P.withCriteria { + sizeGt "children", size order "name" } - then: "We get the correct result back" - results != null - results.size() == 2 - results[0].name == 'Miami' - results[1].name == 'Springfield' + then: "We get the correct owners back" + results*.name == expectedNames - when: "We query for countries with 2 resident" - results = SimpleCountry.withCriteria { - sizeGt "residents", 2 - } - - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Springfield' - - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - sizeGt "residents", 5 - } - - then: "we get no results back" - results.size() == 0 + where: + size | expectedNames + 0 | ['Owner A', 'Owner B', 'Owner C'] + 1 | ['Owner B', 'Owner C'] + 2 | ['Owner C'] + 3 | [] } - void "Test sizeGe criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) - .save(flush: true) - - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") - .save(flush: true) + @Unroll("Test sizeGe criterion with size #size expects #expectedNames") + void "Test sizeGe criterion"(int size, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() - new SimpleCountry(name: "Miami") - .addToResidents(firstName: "Dexter", lastName: "Morgan") - .addToResidents(firstName: "Debra", lastName: "Morgan") - .save(flush: true) - - manager.session.clear() - - when: "We query for countries with 1 resident" - def results = SimpleCountry.withCriteria { - sizeGe "residents", 1 + when: "We query for owners with at least #size children" + def results = Owner_Default_Bi_P.withCriteria { + sizeGe "children", size order "name" } - then: "We get the correct result back" - results != null - results.size() == 3 - results[0].name == 'Dinoville' - results[1].name == 'Miami' - results[2].name == 'Springfield' + then: "We get the correct owners back" + results*.name == expectedNames - when: "We query for countries with 2 resident" - results = SimpleCountry.withCriteria { - sizeGe "residents", 2 - order "name" - } - - then: "We get the correct result back" - results != null - results.size() == 2 - results[0].name == 'Miami' - results[1].name == 'Springfield' - - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - sizeGe "residents", 5 - } - - then: "we get no results back" - results.size() == 0 + where: + size | expectedNames + 1 | ['Owner A', 'Owner B', 'Owner C'] + 2 | ['Owner B', 'Owner C'] + 3 | ['Owner C'] + 4 | [] } - void "Test sizeEq criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) - .save(flush: true) - - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") - .save(flush: true) - - manager.session.clear() - - when: "We query for countries with 1 resident" - def results = SimpleCountry.withCriteria { - sizeEq "residents", 1 - } + @Unroll("Test sizeEq criterion with size #size expects #expectedNames") + void "Test sizeEq criterion"(int size, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Dinoville' - - when: "We query for countries with 3 resident" - results = SimpleCountry.withCriteria { - sizeEq "residents", 3 + when: "We query for owners with exactly #size children" + def results = Owner_Default_Bi_P.withCriteria { + sizeEq "children", size + order "name" } - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Springfield' - - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - sizeEq "residents", 2 - } + then: "We get the correct owners back" + results*.name == expectedNames - then: "we get no results back" - results.size() == 0 + where: + size | expectedNames + 1 | ['Owner A'] + 2 | ['Owner B'] + 3 | ['Owner C'] + 4 | [] } - void "Test sizeNe criterion"() { - given: "A country with only 1 resident" - Person p = new Person(firstName: "Fred", lastName: "Flinstone") - SimpleCountry c = new SimpleCountry(name: "Dinoville") - .addToResidents(p) - .save(flush: true) - - new SimpleCountry(name: "Springfield") - .addToResidents(firstName: "Homer", lastName: "Simpson") - .addToResidents(firstName: "Bart", lastName: "Simpson") - .addToResidents(firstName: "Marge", lastName: "Simpson") - .save(flush: true) - - manager.session.clear() - - when: "We query for countries that don't have 1 resident" - def results = SimpleCountry.withCriteria { - sizeNe "residents", 1 - } - - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Springfield' - - when: "We query for countries who don't have 3 resident" - results = SimpleCountry.withCriteria { - sizeNe "residents", 3 + @Unroll("Test sizeNe criterion for #description expects #expectedNames") + void "Test sizeNe criterion"(String description, Closure queryLogic, List<String> expectedNames) { + given: "A set of owners with 1, 2, and 3 children" + setupTestData() + + when: "We query for owners where the number of children meets a condition" + + def results = Owner_Default_Bi_P.withCriteria { + // Set the delegate of the query closure to the criteria builder and call it + queryLogic.delegate = delegate + queryLogic.call() + order "name" } - then: "We get the correct result back" - results != null - results.size() == 1 - results[0].name == 'Dinoville' - - when: "We query for countries with 2 residents" - results = SimpleCountry.withCriteria { - and { - sizeNe "residents", 1 - sizeNe "residents", 3 - } - } + then: "We get the correct owners back" + results*.name == expectedNames - then: "we get no results back" - results.size() == 0 + where: + description | queryLogic | expectedNames + "size != 1" | { sizeNe "children", 1 } | ['Owner B', 'Owner C'] + "size != 2" | { sizeNe "children", 2 } | ['Owner A', 'Owner C'] + "size != 3" | { sizeNe "children", 3 } | ['Owner A', 'Owner B'] + "size != 1 and != 3" | { and { sizeNe "children", 1; sizeNe "children", 3 } } | ['Owner B'] } -} +} \ No newline at end of file