You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2025/08/29 02:50:04 UTC
(grails-core) 01/04: Better testing coverage for HibernateCriteriaBuilder
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 9c75b40c300a3482a09bf07361bebe6a18d79429
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Aug 28 19:53:42 2025 -0500
Better testing coverage for HibernateCriteriaBuilder
---
.../grails/orm/HibernateCriteriaBuilder.java | 79 +++----
.../GlobalConstraintWithCompositeIdSpec.groovy | 2 +-
.../hibernate/HibernateCriteriaBuilderSpec.groovy | 251 +++++++++++++++++++++
.../HibernateGormStaticApiSpecification.groovy | 235 -------------------
4 files changed, 278 insertions(+), 289 deletions(-)
diff --git a/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
index ac8a7da24c..9ade10998a 100644
--- a/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
+++ b/grails-data-hibernate6/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
@@ -20,8 +20,6 @@ import grails.gorm.MultiTenant;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovy.lang.GroovyObjectSupport;
-import groovy.lang.GroovySystem;
-import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException;
import groovy.util.logging.Slf4j;
@@ -38,11 +36,9 @@ import org.grails.datastore.mapping.query.Query;
import org.grails.datastore.mapping.query.api.BuildableCriteria;
import org.grails.datastore.mapping.query.api.Criteria;
import org.grails.datastore.mapping.query.api.QueryableCriteria;
-import org.grails.datastore.mapping.reflect.NameUtils;
import org.grails.orm.hibernate.AbstractHibernateDatastore;
import org.grails.orm.hibernate.AbstractHibernateSession;
import org.grails.orm.hibernate.GrailsHibernateTemplate;
-import org.grails.orm.hibernate.HibernateDatastore;
import org.grails.orm.hibernate.query.HibernateQuery;
import org.grails.orm.hibernate.query.HibernateQueryConstants;
import org.hibernate.FetchMode;
@@ -191,12 +187,8 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
protected SessionFactory sessionFactory;
protected Session hibernateSession;
protected Class<?> targetClass;
- protected Query.Junction junction = new Query.Conjunction();
protected CriteriaQuery criteriaQuery;
- protected MetaClass criteriaMetaClass;
protected boolean uniqueResult = false;
- protected List<LogicalExpression> logicalExpressionStack = new ArrayList<LogicalExpression>();
- protected List<String> associationStack = new ArrayList<String>();
protected boolean participate;
protected boolean scroll;
protected boolean count;
@@ -250,6 +242,11 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
return this;
}
+ public Query.ProjectionList projections() {
+ return hibernateQuery.projections();
+ }
+
+
/**
* A projection that selects a distince property name
* @param propertyName The property name
@@ -383,7 +380,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
}
public ProjectionList count() {
- return rowCount();
+ return hibernateQuery.projections().count();
}
/**
@@ -1170,11 +1167,21 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
@Override
public Object list(@DelegatesTo(Criteria.class) Closure c) {
+ hibernateQuery.setDetachedCriteria(new DetachedCriteria(targetClass));
return invokeMethod(LIST_CALL, new Object[]{c});
}
+ public List list() {
+ return hibernateQuery.list();
+ }
+
+ public Object singleResult() {
+ return hibernateQuery.singleResult();
+ }
+
@Override
public Object list(Map params, @DelegatesTo(Criteria.class) Closure c) {
+ hibernateQuery.setDetachedCriteria(new DetachedCriteria(targetClass));
return invokeMethod(LIST_CALL, new Object[]{params, c});
}
@@ -1204,6 +1211,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
@SuppressWarnings("rawtypes")
@Override
public Object invokeMethod(String name, Object obj) {
+
Object[] args = obj.getClass().isArray() ? (Object[])obj : new Object[]{obj};
if (paginationEnabledList && SET_RESULT_TRANSFORMER_CALL.equals(name) && args.length == 1 &&
@@ -1230,6 +1238,14 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
// Check for pagination params
if (name.equals(LIST_CALL) && args.length == 2) {
paginationEnabledList = true;
+ if (args[0] instanceof Map map ) {
+ if (map.get("max") instanceof Number max) {
+ hibernateQuery.maxResults(max.intValue());
+ }
+ if (map.get("offset") instanceof Number offset) {
+ hibernateQuery.firstResult(offset.intValue());
+ }
+ }
invokeClosureNode(args[1]);
}
else {
@@ -1281,22 +1297,13 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
return result;
}
- if (criteriaQuery == null) createCriteriaInstance();
MetaMethod metaMethod = getMetaClass().getMetaMethod(name, args);
if (metaMethod != null) {
return metaMethod.invoke(this, args);
}
- MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(targetClass);
- metaMethod = metaClass.getMetaMethod(name, args);
- if (metaMethod != null) {
- return metaMethod.invoke(criteriaQuery, args);
- }
- metaMethod = metaClass.getMetaMethod(NameUtils.getSetterName(name), args);
- if (metaMethod != null) {
- return metaMethod.invoke(criteriaQuery, args);
- }
+
if (isAssociationQueryMethod(args) || isAssociationQueryWithJoinSpecificationMethod(args)) {
final boolean hasMoreThanOneArg = args.length > 1;
@@ -1376,10 +1383,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
throw new MissingMethodException(name, getClass(), args);
}
- protected List createPagedResultList(Map args) {
- GrailsHibernateTemplate ght = new GrailsHibernateTemplate(sessionFactory, (HibernateDatastore) datastore, getDefaultFlushMode());
- return null;
- }
private boolean isAssociationQueryMethod(Object[] args) {
return args.length == 1 && args[0] instanceof Closure;
@@ -1390,16 +1393,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
}
- private String getAssociationPath() {
- StringBuilder fullPath = new StringBuilder();
- for (Object anAssociationStack : associationStack) {
- String propertyName = (String) anAssociationStack;
- if (fullPath.length() > 0) fullPath.append(".");
- fullPath.append(propertyName);
- }
- return fullPath.toString();
- }
-
private boolean isCriteriaConstructionMethod(String name, Object[] args) {
return (name.equals(LIST_CALL) && args.length == 2 && args[0] instanceof Map && args[1] instanceof Closure) ||
(name.equals(ROOT_CALL) ||
@@ -1411,13 +1404,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
name.equals(SCROLL_CALL) && args.length == 1 && args[0] instanceof Closure);
}
- protected void createCriteriaInstance() {
- // no-op
- }
- protected void cacheCriteriaMapping() {
- // no-op
- }
private void invokeClosureNode(Object args) {
Closure<?> callable = (Closure<?>)args;
@@ -1426,20 +1413,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
callable.call();
}
- /**
- * adds and returns the given criterion to the currently active criteria set.
- * this might be either the root criteria or a currently open
- * LogicalExpression.
- */
- protected Query.Criterion addToCriteria(Query.Criterion c) {
- if (false) {
- // logicalExpressionStack.get(logicalExpressionStack.size() - 1).args.add(c);
- }
- else {
- junction.add(c);
- }
- return c;
- }
/**
* Returns the criteria instance
diff --git a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
index c946b43535..387bf4d024 100644
--- a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
+++ b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/compositeid/GlobalConstraintWithCompositeIdSpec.groovy
@@ -36,7 +36,7 @@ class GlobalConstraintWithCompositeIdSpec extends HibernateGormDatastoreSpec {
def setupSpec() {
manager.addAllDomainClasses([ParentB, ChildB, DomainB])
manager.grailsConfig = [
- 'dataSource.url' : "jdbc:tc:postgresql:latest:///dev_db",
+ 'dataSource.url' : "jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000",
'dataSource.dbCreate' : 'create-drop',
'dataSource.formatSql' : 'true',
'dataSource.logSql' : 'true',
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy
new file mode 100644
index 0000000000..77e43baef9
--- /dev/null
+++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateCriteriaBuilderSpec.groovy
@@ -0,0 +1,251 @@
+package org.grails.orm.hibernate
+
+import grails.gorm.annotation.Entity
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.orm.HibernateCriteriaBuilder
+
+import java.math.RoundingMode
+
+class HibernateCriteriaBuilderSpec extends HibernateGormDatastoreSpec {
+
+ def setupSpec() {
+ manager.addAllDomainClasses([Account, Transaction])
+ }
+
+ HibernateCriteriaBuilder builder
+
+ def setup() {
+ def fred = new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock").save(failOnError: true)
+ def barney = new Account(balance: 500, firstName: "Barney", lastName: "Rubble", branch: "Bedrock").save(failOnError: true)
+ new Account(balance: 100, firstName: "Wilma", lastName: "Flintstone", branch: "Bedrock").save(failOnError: true)
+ new Account(balance: 1000, firstName: "Pebbles", lastName: "Flintstone", branch: "Slate Rock and Gravel").save(failOnError: true)
+ new Account(balance: 50, firstName: "Bam-Bam", lastName: "Rubble", branch: null).save(failOnError: true)
+
+ fred.addToTransactions(new Transaction(amount: 10))
+ fred.addToTransactions(new Transaction(amount: 20))
+ fred.save()
+
+ barney.addToTransactions(new Transaction(amount: 50))
+ barney.save(flush: true,failOnError: true)
+
+ builder = new HibernateCriteriaBuilder(Account, manager.hibernateDatastore.sessionFactory, manager.hibernateDatastore)
+ }
+
+ void "test get with eq criteria"() {
+ when:
+ def result = builder.get {
+ eq("firstName", "Fred")
+ }
+
+ then:
+ result.firstName == "Fred"
+ }
+
+ void "test idEq criteria"() {
+ given:
+ def fred = Account.findByFirstName("Fred")
+
+ when:
+ def result = builder.get {
+ idEq(fred.id)
+ }
+
+ then:
+ result.id == fred.id
+ result.firstName == "Fred"
+ }
+
+ void "test list with various criteria"() {
+ when:
+ def results = builder.list {
+ gt("balance", BigDecimal.valueOf(200))
+ or {
+ eq("lastName", "Flintstone")
+ like("branch", "Bedrock")
+ }
+ 'in'("firstName", ["Fred", "Barney", "Pebbles"])
+ }
+
+ then:
+ results.size() == 3
+ results*.firstName.sort() == ["Barney", "Fred", "Pebbles"]
+ }
+
+ void "test ilike criteria"() {
+ when:
+ def results = builder.list {
+ ilike("firstName", "fr%")
+ }
+
+ then:
+ results.size() == 1
+ results[0].firstName == "Fred"
+ }
+
+ void "test isNull criteria"() {
+ when:
+ def results = builder.list {
+ isNull("branch")
+ }
+
+ then:
+ results.size() == 1
+ results[0].firstName == "Bam-Bam"
+ }
+
+ void "test count projection"() {
+ when:
+ def count = builder.get {
+ projections {
+ count()
+ }
+ eq("lastName", "Flintstone")
+ }
+
+ then:
+ count == 3
+ }
+
+ void "test sum and avg projection"() {
+ when:
+ def projections = builder.get {
+ projections {
+ sum('balance')
+ avg('balance')
+ }
+ eq("branch", "Bedrock")
+ }
+
+ then:
+ projections[0] == 850
+ new BigDecimal(projections[1]).setScale(2, RoundingMode.HALF_UP) == 283.33
+ }
+
+ void "test ordering and pagination"() {
+ when:
+ def results = builder.list(max: 2, offset: 1) {
+ order("firstName", "asc")
+ }
+
+ then:
+ results.size() == 2
+ results*.firstName == ["Barney", "Fred"]
+ }
+
+ void "test query on association"() {
+ when:
+ def results = builder.list {
+ transactions {
+ gt("amount", 40)
+ }
+ }
+
+ then:
+ results.size() == 1
+ results[0].firstName == "Barney"
+ }
+
+ void "test inequality and between criteria"() {
+ when:
+ def results = builder.list {
+ ne("firstName", "Fred")
+ ge("balance", BigDecimal.valueOf(60))
+ le("balance", BigDecimal.valueOf(1000))
+ }
+ then:
+ results*.firstName.toSet() == ["Barney","Wilma","Pebbles"] as Set
+ }
+
+ void "test isNotNull and size constraints on association"() {
+ when:
+ def results = builder.list {
+ isNotNull("branch")
+ sizeGe("transactions", 1)
+ }
+ then:
+ results*.firstName.toSet() == ["Fred","Barney"] as Set
+ }
+
+ void "test property to property comparisons and ordering desc"() {
+ when:
+ def results = builder.list {
+ geProperty("balance", "balance") // always true, validates path
+ order("balance", "desc")
+ }
+ then:
+ results[0].firstName == "Pebbles"
+ results[-1].firstName == "Bam-Bam"
+ }
+
+ void "test projections countDistinct groupProperty min max"() {
+ when:
+ def results = builder.list {
+ projections {
+ groupProperty("lastName")
+ countDistinct("firstName")
+ min("balance")
+ max("balance")
+ }
+ }
+ then:
+ results.size() >= 1
+ }
+
+ void "test inList array and collection variants and firstResult"() {
+ when:
+ def list1 = builder.list {
+ 'in'("firstName", ["Fred", "Barney"] as Object[]) // array via 'in' alias
+ }
+ def list2 = builder.list {
+ inList("firstName", ["Fred", "Wilma"] as Object[]) // array variant
+ }
+ def paged = builder.list(max: 1) {
+ order("firstName", "asc")
+ firstResult(2)
+ }
+ then:
+ list1 instanceof List && list1.size() > 0
+ list2 instanceof List && list2.size() > 0
+ paged.size() == 1
+ paged[0].firstName == "Fred"
+ }
+
+ void "test eq with ignoreCase param path and like/ilike methods"() {
+ when:
+ def results = builder.list {
+ eq("firstName", "Fr", [ignoreCase:true]) // should fallback to like with %Fr%
+ }
+ def likeRes = builder.list {
+ like("branch", "%Bedrock%")
+ }
+ def ilikeRes = builder.list {
+ ilike("branch", "%BEDROCK%")
+ }
+ then:
+ results instanceof List
+ likeRes.size() >= 1
+ ilikeRes.size() >= 1
+ }
+}
+
+@Entity
+class Account {
+ String firstName
+ String lastName
+ BigDecimal balance
+ String branch
+ Set<Transaction> transactions
+
+ static hasMany = [transactions: Transaction]
+ static constraints = {
+ branch nullable:true
+ }
+}
+
+@Entity
+class Transaction {
+ BigDecimal amount
+ Date dateCreated
+
+ static belongsTo = [account: Account]
+}
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy
deleted file mode 100644
index e151ac3b20..0000000000
--- a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpecification.groovy
+++ /dev/null
@@ -1,235 +0,0 @@
-package org.grails.orm.hibernate
-
-import grails.gorm.specs.HibernateGormDatastoreSpec
-import grails.persistence.Entity
-import groovy.transform.EqualsAndHashCode
-import org.hibernate.ScrollableResults
-import org.hibernate.transform.ToListResultTransformer
-import spock.lang.Ignore
-
-
-class HibernateGormStaticApiTest extends HibernateGormDatastoreSpec{
-
- def setupSpec() {
- manager.addAllDomainClasses([Account, Transaction, Whatever])
- }
-
- Account fredAccount
- Account barneyAccount
- Account jamesAccount
- Account maryAccount
- def accountCriteria
-
- def setup() {
- fredAccount = new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock")
- fredAccount.transactions << new Transaction(amount: 50, account: fredAccount)
- fredAccount.save(flush:true)
- barneyAccount = new Account(balance: 500, firstName: "Barney", lastName: "Rubble", branch: "Bedrock").save(flush: true)
- jamesAccount = new Account(balance: 750, firstName: "James", lastName: "Bond", branch: "London").save(flush: true)
- maryAccount = new Account(balance: 1000, firstName: "Mary", lastName: "Poppins", branch: "London").save(flush: true)
- accountCriteria = Account.createCriteria()
- }
-
- def "equalsCriteria"() {
- when:
- def results = accountCriteria {
- eq("branch","London")
- maxResults(1)
- }
- then:
- results.size() == 1
- results[0] == jamesAccount
- }
-
- def "between"() {
- when:
- def results = accountCriteria {
- between("balance", 500, 1000)
- order("balance", "asc")
- }
- then:
- results.size() == 3
- results[0] == barneyAccount
- results[1] == jamesAccount
- results[2] == maryAccount
- }
-
- def "or"() {
- when:
- def results = accountCriteria {
- or {
- like("firstName", "Fred%")
- like("firstName", "Barney%")
-
- }
- order("balance", "asc")
- }
- then:
- results.size() == 2
- results[0] == fredAccount
- results[1] == barneyAccount
- }
-
- @Ignore("JPA Criteria requires a single predicate behind a not")
- def "not"() {
- when:
- def results = accountCriteria {
- not {
- between("balance", 500, 1000)
- eq("branch", "London")
-
- }
- order("balance", "asc")
- }
- then:
- results.size() == 1
- results[0] == fredAccount
- }
-
- def transactions() {
- when:
- def results = accountCriteria.list {
- between("balance", 0, 500)
- transactions {
- between("amount", 50, 100)
- }
- }
- then:
- results.size() == 1
- results[0] == fredAccount
- }
-
- def projectionsList() {
- when:
- def results = accountCriteria.get {
- projections {
- countDistinct('branch')
- countDistinct('balance')
- }
- }
- then:
- results.size() == 2
- results[0] == 2
- results[1] == 4
- }
-
- def projections() {
- when:
- def results = accountCriteria.get {
- projections {
- countDistinct('branch')
- }
- }
- then:
- results == 2
- }
-
- def scroll() {
- when:
- ScrollableResults results = accountCriteria.scroll {
- order("balance", "asc")
- }
- then:
- results.scroll(1)
- fredAccount == results.get()
- results.last()
- maryAccount == results.get()
- results.previous()
- jamesAccount == results.get()
- results.first()
- fredAccount == results.get()
- results.next()
- barneyAccount == results.get()
- }
-
- def "transformer"() {
- when:
- List results = accountCriteria.get {
- resultTransformer(ToListResultTransformer.INSTANCE)
- projections {
- countDistinct('branch')
- countDistinct('balance')
- }
- }
- then:
- results[0] == 2
- results[1] == 4
- }
-
- def eagerFetching() {
- when:
- def results = accountCriteria.list {
- eq("branch", "Bedrock")
- join("transactions")
- }
- then:
- results.size() == 1
- results[0] == fredAccount
- results[0].transactions.size() == 1
- results[0].transactions[0].amount == 50
- }
-
- def listDistinct() {
- new Account(balance: 250, firstName: "Fred", lastName: "Flintstone", branch: "Bedrock").save(flush:true)
- when:
- def results = accountCriteria.listDistinct {
- property("branch")
- order("branch")
- }
- then:
- results.size() == 2
- results[0] == "Bedrock"
- results[1] == "London"
- }
-
-//TODO: Chained Criteria
- def multipleCriteria() {
- given:
- def accountCriteria = {
- eq "branch", "Bedrock"
- }
- when:
- def results = Account.withCriteria {
- accountCriteria.delegate = accountCriteria
- accountCriteria()
- transactions {
- between("amount", 50, 100)
- }
- }
- then:
- results.size() == 1
- results[0] == fredAccount
- results[0].transactions.size() == 1
- results[0].transactions[0].amount == 50
-
- }
-
-
-
-
-}
-
-@Entity
-@EqualsAndHashCode
-class Whatever {
- Integer branch
- Integer balance
-}
-
-@Entity
-class Transaction {
- BigDecimal amount
- static belongsTo = [account: Account]
-}
-
-@Entity
-@EqualsAndHashCode
-class Account {
- BigDecimal balance
- String branch
- String firstName
- String lastName
- Set<Transaction> transactions = new HashSet<>()
- static hasMany = [transactions:Transaction]
-
-}
\ No newline at end of file