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 2026/03/09 18:05:06 UTC
(grails-core) 34/34: hibernate7: added lock and fetchMode support to CriteriaMethodInvoker
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 91b95a39da5b6f01a2d233f6c46c87d6f4bf831d
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Mar 8 20:00:48 2026 -0500
hibernate7:
added lock and fetchMode support to CriteriaMethodInvoker
---
.../groovy/grails/orm/CriteriaMethodInvoker.java | 562 +++++++++++----------
.../main/groovy/grails/orm/CriteriaMethods.java | 4 +-
.../grails/orm/HibernateCriteriaBuilder.java | 45 +-
.../grails/orm/CriteriaMethodInvokerSpec.groovy | 31 +-
.../orm/HibernateCriteriaBuilderDirectSpec.groovy | 50 ++
.../grails/orm/HibernateCriteriaBuilderSpec.groovy | 30 ++
.../src/en/ref/Domain Classes/createCriteria.adoc | 6 +-
7 files changed, 432 insertions(+), 296 deletions(-)
diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
index 5b28247255..64ef644a23 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
@@ -20,331 +20,343 @@ package grails.orm;
import grails.gorm.DetachedCriteria;
import grails.gorm.PagedResultList;
+
import groovy.lang.Closure;
import groovy.lang.MetaMethod;
+
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
+
import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.Map;
+
import org.grails.datastore.mapping.query.Query;
import org.grails.orm.hibernate.query.HibernateQuery;
import org.grails.orm.hibernate.query.HibernateQueryArgument;
+
import org.springframework.beans.BeanUtils;
public class CriteriaMethodInvoker {
- private static final Object UNHANDLED = new Object();
+ private static final Object UNHANDLED = new Object();
- private final HibernateCriteriaBuilder builder;
+ private final HibernateCriteriaBuilder builder;
- public CriteriaMethodInvoker(HibernateCriteriaBuilder builder) {
- this.builder = builder;
- }
-
- public Object invokeMethod(String name, Object... args) {
- CriteriaMethods method = CriteriaMethods.fromName(name);
+ public CriteriaMethodInvoker(HibernateCriteriaBuilder builder) {
+ this.builder = builder;
+ }
- Object result = tryCriteriaConstruction(method, args);
- if (result != UNHANDLED) return result;
+ public Object invokeMethod(String name, Object... args) {
+ CriteriaMethods method = CriteriaMethods.fromName(name);
- result = tryMetaMethod(name, args);
- if (result != UNHANDLED) return result;
+ Object result = tryCriteriaConstruction(method, args);
+ if (result != UNHANDLED) return result;
- result = tryAssociationOrJunction(name, method, args);
- if (result != UNHANDLED) return result;
+ result = tryMetaMethod(name, args);
+ if (result != UNHANDLED) return result;
- result = trySimpleCriteria(name, method, args);
- if (result != UNHANDLED) return result;
+ result = tryAssociationOrJunction(name, method, args);
+ if (result != UNHANDLED) return result;
- result = tryPropertyCriteria(method, args);
- if (result != UNHANDLED) return result;
+ result = trySimpleCriteria(name, method, args);
+ if (result != UNHANDLED) return result;
- return CriteriaMethods.fromName(name, HibernateCriteriaBuilder.class, args);
- }
+ result = tryPropertyCriteria(method, args);
+ if (result != UNHANDLED) return result;
- private Object tryCriteriaConstruction(CriteriaMethods method, Object... args) {
- if (method == null || !isCriteriaConstructionMethod(method, args)) {
- return UNHANDLED;
+ return CriteriaMethods.fromName(name, HibernateCriteriaBuilder.class, args);
}
- HibernateQuery hibernateQuery = builder.getHibernateQuery();
- switch (method) {
- case GET_CALL -> builder.setUniqueResult(true);
- case SCROLL_CALL -> builder.setScroll(true);
- case COUNT_CALL -> builder.setCount(true);
- case LIST_DISTINCT_CALL -> builder.setDistinct(true);
- default -> {}
- }
+ private Object tryCriteriaConstruction(CriteriaMethods method, Object... args) {
+ if (method == null || !isCriteriaConstructionMethod(method, args)) {
+ return UNHANDLED;
+ }
- // Check for pagination params
- if (method == CriteriaMethods.LIST_CALL && args.length == 2) {
- builder.setPaginationEnabledList(true);
- if (args[0] instanceof Map<?, ?> map) {
- if (map.get("max") instanceof Number max) {
- hibernateQuery.maxResults(max.intValue());
+ HibernateQuery hibernateQuery = builder.getHibernateQuery();
+ switch (method) {
+ case GET_CALL -> builder.setUniqueResult(true);
+ case SCROLL_CALL -> builder.setScroll(true);
+ case COUNT_CALL -> builder.setCount(true);
+ case LIST_DISTINCT_CALL -> builder.setDistinct(true);
+ default -> {
+ }
}
- if (map.get("offset") instanceof Number offset) {
- hibernateQuery.firstResult(offset.intValue());
+
+ // Check for pagination params
+ if (method == CriteriaMethods.LIST_CALL && args.length == 2) {
+ builder.setPaginationEnabledList(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 {
+ invokeClosureNode(args[0]);
}
- }
- invokeClosureNode(args[1]);
- } else {
- invokeClosureNode(args[0]);
- }
- Object result;
- if (!builder.isUniqueResult()) {
- if (builder.isDistinct()) {
- hibernateQuery.distinct();
- result = hibernateQuery.list();
- } else if (builder.isCount()) {
- hibernateQuery.projections().count();
- result = hibernateQuery.singleResult();
- } else if (builder.isPaginationEnabledList()) {
- Map<?, ?> argMap = (Map<?, ?>) args[0];
- final String sortField = (String) argMap.get(HibernateQueryArgument.SORT.value());
- if (sortField != null) {
- final boolean ignoreCase =
- !(argMap.get(HibernateQueryArgument.IGNORE_CASE.value()) instanceof Boolean b) || b;
- final String orderParam = (String) argMap.get(HibernateQueryArgument.ORDER.value());
- final Query.Order.Direction direction =
- Query.Order.Direction.DESC.name().equalsIgnoreCase(orderParam)
- ? Query.Order.Direction.DESC
- : Query.Order.Direction.ASC;
- Query.Order order;
- order = new Query.Order(sortField, direction);
- if (ignoreCase) {
- order.ignoreCase();
- }
- hibernateQuery.order(order);
+ Object result;
+ if (!builder.isUniqueResult()) {
+ if (builder.isDistinct()) {
+ hibernateQuery.distinct();
+ result = hibernateQuery.list();
+ } else if (builder.isCount()) {
+ hibernateQuery.projections().count();
+ result = hibernateQuery.singleResult();
+ } else if (builder.isPaginationEnabledList()) {
+ Map<?, ?> argMap = (Map<?, ?>) args[0];
+ final String sortField = (String) argMap.get(HibernateQueryArgument.SORT.value());
+ if (sortField != null) {
+ final boolean ignoreCase =
+ !(argMap.get(HibernateQueryArgument.IGNORE_CASE.value()) instanceof Boolean b) || b;
+ final String orderParam = (String) argMap.get(HibernateQueryArgument.ORDER.value());
+ final Query.Order.Direction direction =
+ Query.Order.Direction.DESC.name().equalsIgnoreCase(orderParam)
+ ? Query.Order.Direction.DESC
+ : Query.Order.Direction.ASC;
+ Query.Order order;
+ order = new Query.Order(sortField, direction);
+ if (ignoreCase) {
+ order.ignoreCase();
+ }
+ hibernateQuery.order(order);
+ }
+ result = new PagedResultList<>(hibernateQuery);
+ } else if (builder.isScroll()) {
+ result = hibernateQuery.scroll();
+ } else {
+ result = hibernateQuery.list();
+ }
+ } else {
+ result = hibernateQuery.singleResult();
}
- result = new PagedResultList<>(hibernateQuery);
- } else if (builder.isScroll()) {
- result = hibernateQuery.scroll();
- } else {
- result = hibernateQuery.list();
- }
- } else {
- result = hibernateQuery.singleResult();
- }
- if (!builder.isParticipate()) {
- builder.closeSession();
+ if (!builder.isParticipate()) {
+ builder.closeSession();
+ }
+ return result;
}
- return result;
- }
- private Object tryMetaMethod(String name, Object... args) {
- MetaMethod metaMethod = builder.getMetaClass().getMetaMethod(name, args);
- if (metaMethod != null) {
- return metaMethod.invoke(builder, args);
+ private Object tryMetaMethod(String name, Object... args) {
+ MetaMethod metaMethod = builder.getMetaClass().getMetaMethod(name, args);
+ if (metaMethod != null) {
+ return metaMethod.invoke(builder, args);
+ }
+ return UNHANDLED;
}
- return UNHANDLED;
- }
- @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
- private Object tryAssociationOrJunction(String name, CriteriaMethods method, Object... args) {
- if (!isAssociationQueryMethod(args) && !isAssociationQueryWithJoinSpecificationMethod(args)) {
- return UNHANDLED;
+ @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+ private Object tryAssociationOrJunction(String name, CriteriaMethods method, Object... args) {
+ if (!isAssociationQueryMethod(args) && !isAssociationQueryWithJoinSpecificationMethod(args)) {
+ return UNHANDLED;
+ }
+
+ final boolean hasMoreThanOneArg = args.length > 1;
+ final Closure<?> callable = hasMoreThanOneArg ? (Closure<?>) args[1] : (Closure<?>) args[0];
+ final HibernateQuery hibernateQuery = builder.getHibernateQuery();
+
+ if (method != null) {
+ switch (method) {
+ case AND:
+ hibernateQuery.and(callable);
+ return name;
+ case OR:
+ hibernateQuery.or(callable);
+ return name;
+ case NOT:
+ hibernateQuery.not(callable);
+ return name;
+ case PROJECTIONS:
+ if (args.length == 1 && (args[0] instanceof Closure)) {
+ invokeClosureNode(callable);
+ return name;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ final PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(builder.getTargetClass(), name);
+ if (pd != null && pd.getReadMethod() != null) {
+ final Metamodel metamodel = builder.getSessionFactory().getMetamodel();
+ final EntityType<?> entityType = metamodel.entity(builder.getTargetClass());
+ final Attribute<?, ?> attribute = entityType.getAttribute(name);
+
+ if (attribute.isAssociation()) {
+ Class<?> oldTargetClass = builder.getTargetClass();
+ builder.setTargetClass(builder.getClassForAssociationType(attribute));
+ JoinType joinType;
+ if (hasMoreThanOneArg) {
+ joinType = builder.convertFromInt((Integer) args[0]);
+ } else if (builder.getTargetClass().equals(oldTargetClass)) {
+ joinType = JoinType.LEFT; // default to left join if joining on the same table
+ } else {
+ joinType = builder.convertFromInt(0);
+ }
+
+ hibernateQuery.join(name, joinType);
+ hibernateQuery.in(name, new DetachedCriteria<>(builder.getTargetClass()).build(callable));
+ builder.setTargetClass(oldTargetClass);
+
+ return name;
+ }
+ }
+ return UNHANDLED;
}
- final boolean hasMoreThanOneArg = args.length > 1;
- final Closure<?> callable = hasMoreThanOneArg ? (Closure<?>) args[1] : (Closure<?>) args[0];
- final HibernateQuery hibernateQuery = builder.getHibernateQuery();
-
- if (method != null) {
- switch (method) {
- case AND:
- hibernateQuery.and(callable);
- return name;
- case OR:
- hibernateQuery.or(callable);
- return name;
- case NOT:
- hibernateQuery.not(callable);
- return name;
- case PROJECTIONS:
- if (args.length == 1 && (args[0] instanceof Closure)) {
- invokeClosureNode(callable);
- return name;
- }
- break;
- default:
- break;
- }
+ @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+ protected Object trySimpleCriteria(String name, CriteriaMethods method, Object... args) {
+ if (args.length != 1 || args[0] == null) {
+ return UNHANDLED;
+ }
+
+ if (method != null) {
+ switch (method) {
+ case ID_EQUALS:
+ return builder.eq("id", args[0]);
+ case CACHE:
+ if (args[0] instanceof Boolean b) {
+ builder.cache(b);
+ }
+ return name;
+ case READ_ONLY:
+ if (args[0] instanceof Boolean b) {
+ builder.readOnly(b);
+ }
+ return name;
+ case SINGLE_RESULT:
+ return builder.singleResult();
+ case IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY:
+ if (!(args[0] instanceof String)) {
+ builder.throwRuntimeException(
+ new IllegalArgumentException(
+ "call to ["
+ + name
+ + "] with value ["
+ + args[0]
+ + "] requires a String value."));
+ }
+ final String value = (String) args[0];
+ switch (method) {
+ case IS_NULL -> builder.getHibernateQuery().isNull(value);
+ case IS_NOT_NULL -> builder.getHibernateQuery().isNotNull(value);
+ case IS_EMPTY -> builder.getHibernateQuery().isEmpty(value);
+ case IS_NOT_EMPTY -> builder.getHibernateQuery().isNotEmpty(value);
+ default -> {
+ }
+ }
+ return name;
+ default:
+ break;
+ }
+ }
+ return UNHANDLED;
}
- final PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(builder.getTargetClass(), name);
- if (pd != null && pd.getReadMethod() != null) {
- final Metamodel metamodel = builder.getSessionFactory().getMetamodel();
- final EntityType<?> entityType = metamodel.entity(builder.getTargetClass());
- final Attribute<?, ?> attribute = entityType.getAttribute(name);
-
- if (attribute.isAssociation()) {
- Class<?> oldTargetClass = builder.getTargetClass();
- builder.setTargetClass(builder.getClassForAssociationType(attribute));
- JoinType joinType;
- if (hasMoreThanOneArg) {
- joinType = builder.convertFromInt((Integer) args[0]);
- } else if (builder.getTargetClass().equals(oldTargetClass)) {
- joinType = JoinType.LEFT; // default to left join if joining on the same table
- } else {
- joinType = builder.convertFromInt(0);
+ @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
+ protected Object tryPropertyCriteria(CriteriaMethods method, Object... args) {
+ if (method == CriteriaMethods.FETCH_MODE) {
+ if (args.length == 2 && args[0] instanceof String s && args[1] instanceof org.hibernate.FetchMode fm) {
+ builder.fetchMode(s, fm);
+ return "fetchMode";
+ }
}
- hibernateQuery.join(name, joinType);
- hibernateQuery.in(name, new DetachedCriteria<>(builder.getTargetClass()).build(callable));
- builder.setTargetClass(oldTargetClass);
+ if (method == null || args.length < 2 || !(args[0] instanceof String propertyName)) {
+ return UNHANDLED;
+ }
- return name;
- }
+ switch (method) {
+ case RLIKE:
+ return builder.rlike(propertyName, args[1]);
+ case BETWEEN:
+ if (args.length >= 3) {
+ return builder.between(propertyName, args[1], args[2]);
+ }
+ break;
+ case EQUALS:
+ if (args.length == 3 && args[2] instanceof Map<?, ?> map) {
+ return builder.eq(propertyName, args[1], map);
+ }
+ return builder.eq(propertyName, args[1]);
+ case EQUALS_PROPERTY:
+ return builder.eqProperty(propertyName, args[1].toString());
+ case GREATER_THAN:
+ return builder.gt(propertyName, args[1]);
+ case GREATER_THAN_PROPERTY:
+ return builder.gtProperty(propertyName, args[1].toString());
+ case GREATER_THAN_OR_EQUAL:
+ return builder.ge(propertyName, args[1]);
+ case GREATER_THAN_OR_EQUAL_PROPERTY:
+ return builder.geProperty(propertyName, args[1].toString());
+ case ILIKE:
+ return builder.ilike(propertyName, args[1]);
+ case IN:
+ if (args[1] instanceof Collection) {
+ return builder.in(propertyName, (Collection<?>) args[1]);
+ } else if (args[1] instanceof Object[]) {
+ return builder.in(propertyName, (Object[]) args[1]);
+ }
+ break;
+ case LESS_THAN:
+ return builder.lt(propertyName, args[1]);
+ case LESS_THAN_PROPERTY:
+ return builder.ltProperty(propertyName, args[1].toString());
+ case LESS_THAN_OR_EQUAL:
+ return builder.le(propertyName, args[1]);
+ case LESS_THAN_OR_EQUAL_PROPERTY:
+ return builder.leProperty(propertyName, args[1].toString());
+ case LIKE:
+ return builder.like(propertyName, args[1]);
+ case NOT_EQUAL:
+ return builder.ne(propertyName, args[1]);
+ case NOT_EQUAL_PROPERTY:
+ return builder.neProperty(propertyName, args[1].toString());
+ case SIZE_EQUALS:
+ if (args[1] instanceof Number) {
+ return builder.sizeEq(propertyName, ((Number) args[1]).intValue());
+ }
+ break;
+ default:
+ break;
+ }
+ return UNHANDLED;
}
- return UNHANDLED;
- }
- @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
- protected Object trySimpleCriteria(String name, CriteriaMethods method, Object... args) {
- if (args.length != 1 || args[0] == null) {
- return UNHANDLED;
+ private boolean isAssociationQueryMethod(Object... args) {
+ return args.length == 1 && args[0] instanceof Closure;
}
- if (method != null) {
- switch (method) {
- case ID_EQUALS:
- return builder.eq("id", args[0]);
- case CACHE:
- if (args[0] instanceof Boolean b) {
- builder.cache(b);
- }
- return name;
- case READ_ONLY:
- if (args[0] instanceof Boolean b) {
- builder.readOnly(b);
- }
- return name;
- case IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY:
- if (!(args[0] instanceof String)) {
- builder.throwRuntimeException(
- new IllegalArgumentException(
- "call to ["
- + name
- + "] with value ["
- + args[0]
- + "] requires a String value."));
- }
- final String value = (String) args[0];
- switch (method) {
- case IS_NULL ->
- builder.getHibernateQuery().isNull(value);
- case IS_NOT_NULL ->
- builder.getHibernateQuery().isNotNull(value);
- case IS_EMPTY ->
- builder.getHibernateQuery().isEmpty(value);
- case IS_NOT_EMPTY ->
- builder.getHibernateQuery().isNotEmpty(value);
- default -> {}
- }
- return name;
- default:
- break;
- }
+ private boolean isAssociationQueryWithJoinSpecificationMethod(Object... args) {
+ return args.length == 2 && (args[0] instanceof Number) && (args[1] instanceof Closure);
}
- return UNHANDLED;
- }
- @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
- protected Object tryPropertyCriteria(CriteriaMethods method, Object... args) {
- if (method == null || args.length < 2 || !(args[0] instanceof String propertyName)) {
- return UNHANDLED;
+ private boolean isCriteriaConstructionMethod(CriteriaMethods method, Object... args) {
+ return (method == CriteriaMethods.LIST_CALL
+ && args.length == 2
+ && args[0] instanceof Map<?, ?>
+ && args[1] instanceof Closure)
+ || (method == CriteriaMethods.ROOT_CALL
+ || method == CriteriaMethods.ROOT_DO_CALL
+ || method == CriteriaMethods.LIST_CALL
+ || method == CriteriaMethods.LIST_DISTINCT_CALL
+ || method == CriteriaMethods.GET_CALL
+ || method == CriteriaMethods.COUNT_CALL
+ || (method == CriteriaMethods.SCROLL_CALL
+ && args.length == 1
+ && args[0] instanceof Closure));
}
- switch (method) {
- case RLIKE:
- return builder.rlike(propertyName, args[1]);
- case BETWEEN:
- if (args.length >= 3) {
- return builder.between(propertyName, args[1], args[2]);
- }
- break;
- case EQUALS:
- if (args.length == 3 && args[2] instanceof Map<?, ?> map) {
- return builder.eq(propertyName, args[1], map);
- }
- return builder.eq(propertyName, args[1]);
- case EQUALS_PROPERTY:
- return builder.eqProperty(propertyName, args[1].toString());
- case GREATER_THAN:
- return builder.gt(propertyName, args[1]);
- case GREATER_THAN_PROPERTY:
- return builder.gtProperty(propertyName, args[1].toString());
- case GREATER_THAN_OR_EQUAL:
- return builder.ge(propertyName, args[1]);
- case GREATER_THAN_OR_EQUAL_PROPERTY:
- return builder.geProperty(propertyName, args[1].toString());
- case ILIKE:
- return builder.ilike(propertyName, args[1]);
- case IN:
- if (args[1] instanceof Collection) {
- return builder.in(propertyName, (Collection<?>) args[1]);
- } else if (args[1] instanceof Object[]) {
- return builder.in(propertyName, (Object[]) args[1]);
- }
- break;
- case LESS_THAN:
- return builder.lt(propertyName, args[1]);
- case LESS_THAN_PROPERTY:
- return builder.ltProperty(propertyName, args[1].toString());
- case LESS_THAN_OR_EQUAL:
- return builder.le(propertyName, args[1]);
- case LESS_THAN_OR_EQUAL_PROPERTY:
- return builder.leProperty(propertyName, args[1].toString());
- case LIKE:
- return builder.like(propertyName, args[1]);
- case NOT_EQUAL:
- return builder.ne(propertyName, args[1]);
- case NOT_EQUAL_PROPERTY:
- return builder.neProperty(propertyName, args[1].toString());
- case SIZE_EQUALS:
- if (args[1] instanceof Number) {
- return builder.sizeEq(propertyName, ((Number) args[1]).intValue());
- }
- break;
- default:
- break;
+ private void invokeClosureNode(Object args) {
+ Closure<?> callable = (Closure<?>) args;
+ callable.setDelegate(builder);
+ callable.setResolveStrategy(Closure.DELEGATE_FIRST);
+ callable.call();
}
- return UNHANDLED;
- }
-
- private boolean isAssociationQueryMethod(Object... args) {
- return args.length == 1 && args[0] instanceof Closure;
- }
-
- private boolean isAssociationQueryWithJoinSpecificationMethod(Object... args) {
- return args.length == 2 && (args[0] instanceof Number) && (args[1] instanceof Closure);
- }
-
- private boolean isCriteriaConstructionMethod(CriteriaMethods method, Object... args) {
- return (method == CriteriaMethods.LIST_CALL
- && args.length == 2
- && args[0] instanceof Map<?, ?>
- && args[1] instanceof Closure)
- || (method == CriteriaMethods.ROOT_CALL
- || method == CriteriaMethods.ROOT_DO_CALL
- || method == CriteriaMethods.LIST_CALL
- || method == CriteriaMethods.LIST_DISTINCT_CALL
- || method == CriteriaMethods.GET_CALL
- || method == CriteriaMethods.COUNT_CALL
- || (method == CriteriaMethods.SCROLL_CALL
- && args.length == 1
- && args[0] instanceof Closure));
- }
-
- private void invokeClosureNode(Object args) {
- Closure<?> callable = (Closure<?>) args;
- callable.setDelegate(builder);
- callable.setResolveStrategy(Closure.DELEGATE_FIRST);
- callable.call();
- }
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
index 46d0f4b35f..ab98bae51e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
@@ -59,7 +59,9 @@ public enum CriteriaMethods {
SCROLL_CALL("scroll"),
PROJECTIONS("projections"),
CACHE("cache"),
- READ_ONLY("readOnly");
+ READ_ONLY("readOnly"),
+ FETCH_MODE("fetchMode"),
+ SINGLE_RESULT("singleResult");
private final String name;
diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
index 62221c4df1..3cc4a2d0f7 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
@@ -44,6 +44,8 @@ import org.grails.orm.hibernate.GrailsHibernateTemplate;
import org.grails.orm.hibernate.query.HibernateQuery;
import org.hibernate.FetchMode;
import org.hibernate.SessionFactory;
+import org.springframework.orm.hibernate5.SessionHolder;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* Implements the GORM criteria DSL for Hibernate 7+. The builder exposes a Groovy-closure DSL that
@@ -66,9 +68,24 @@ import org.hibernate.SessionFactory;
* }
* maxResults(10)
* order("holderLastName", "desc")
+ * cache(true)
+ * readOnly(true)
* }
* </pre>
*
+ * <h2>Advanced Features</h2>
+ *
+ * <p>The builder supports several advanced Hibernate features:
+ *
+ * <ul>
+ * <li><b>Pessimistic Locking:</b> Use {@code lock(true)} to obtain a pessimistic write lock.
+ * <li><b>Query Caching:</b> Use {@code cache(true)} to enable query caching for the results.
+ * <li><b>Read-Only Mode:</b> Use {@code readOnly(true)} to disable dirty checking for loaded
+ * entities.
+ * <li><b>Fetch Mode:</b> Use {@code fetchMode("association", FetchMode.JOIN)} to specify Eager/Lazy
+ * fetching strategies.
+ * </ul>
+ *
* <h2>Programmatic instantiation</h2>
*
* <p>The builder requires a {@link SessionFactory}, the target persistent class, and the {@link
@@ -104,8 +121,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
private Class<?> targetClass;
private CriteriaQuery<?> criteriaQuery;
private boolean uniqueResult = false;
- //TODO Transactional behaviour is currently not supported, but we need to track whether the criteria should participate in a transaction or not
- private boolean participate;
+ private final boolean participate;
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
private boolean scroll;
@@ -117,11 +133,6 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
private int defaultFlushMode;
private final org.hibernate.query.criteria.HibernateCriteriaBuilder cb;
private final HibernateQuery hibernateQuery;
- private boolean shouldLock;
- private boolean shouldCache;
-
- @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
- private boolean readOnly;
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
private boolean distinct = false;
@@ -133,6 +144,13 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
setDatastore(datastore);
this.sessionFactory = sessionFactory;
this.cb = sessionFactory.getCriteriaBuilder();
+ if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
+ this.participate = true;
+ } else {
+ this.participate = false;
+ org.hibernate.Session session = sessionFactory.openSession();
+ TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
+ }
HibernateSession session = (HibernateSession) datastore.connect();
hibernateQuery =
new HibernateQuery(
@@ -208,7 +226,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
* @param shouldLock True if it should
*/
public void lock(boolean shouldLock) {
- this.shouldLock = shouldLock;
+ hibernateQuery.lock(shouldLock);
}
/**
@@ -229,7 +247,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
*/
@Override
public BuildableCriteria cache(boolean shouldCache) {
- this.shouldCache = shouldCache;
+ hibernateQuery.cache(shouldCache);
return this;
}
@@ -245,7 +263,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
*/
@Override
public BuildableCriteria readOnly(boolean readOnly) {
- this.readOnly = readOnly;
+ hibernateQuery.setReadOnly(readOnly);
return this;
}
@@ -1272,6 +1290,13 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
/** Closes the session if it is copen */
public void closeSession() {
+ if (!participate) {
+ SessionHolder sessionHolder =
+ (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
+ if (sessionHolder.getSession().isOpen()) {
+ sessionHolder.getSession().close();
+ }
+ }
hibernateQuery.getSession().disconnect();
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
index 978c64532c..02b713486d 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
@@ -3,13 +3,18 @@ package grails.orm
import groovy.lang.Closure
import groovy.lang.MissingMethodException
import org.grails.orm.hibernate.query.HibernateQuery
+import org.grails.orm.hibernate.HibernateSession
+import org.hibernate.Session
import org.hibernate.SessionFactory
+import org.springframework.orm.hibernate5.SessionHolder
+import org.springframework.transaction.support.TransactionSynchronizationManager
import spock.lang.Specification
class CriteriaMethodInvokerSpec extends Specification {
HibernateCriteriaBuilder builder = Mock(HibernateCriteriaBuilder)
HibernateQuery query = Mock(HibernateQuery)
+ SessionFactory sessionFactory = Mock(SessionFactory)
CriteriaMethodInvoker invoker = new CriteriaMethodInvoker(builder)
def setup() {
@@ -195,6 +200,22 @@ class CriteriaMethodInvokerSpec extends Specification {
1 * builder.readOnly(true)
}
+ void "trySimpleCriteria: singleResult delegates to builder.singleResult"() {
+ when:
+ invoker.trySimpleCriteria('singleResult', CriteriaMethods.SINGLE_RESULT, [42L] as Object[])
+
+ then:
+ 1 * builder.singleResult()
+ }
+
+ void "tryPropertyCriteria: fetchMode delegates to builder.fetchMode"() {
+ when:
+ invoker.tryPropertyCriteria(CriteriaMethods.FETCH_MODE, ["transactions", org.hibernate.FetchMode.JOIN] as Object[])
+
+ then:
+ 1 * builder.fetchMode("transactions", org.hibernate.FetchMode.JOIN)
+ }
+
void "trySimpleCriteria: isNull with String delegates to hibernateQuery.isNull"() {
when:
invoker.trySimpleCriteria('isNull', CriteriaMethods.IS_NULL, ['branch'] as Object[])
@@ -435,17 +456,9 @@ class CriteriaMethodInvokerSpec extends Specification {
result != null // UNHANDLED sentinel
0 * builder._
}
-
- void "tryPropertyCriteria: non-String first arg returns UNHANDLED"() {
- when:
- def result = invoker.tryPropertyCriteria(CriteriaMethods.EQUALS, [42, 'Fred'] as Object[])
-
- then:
- result != null // UNHANDLED sentinel
- 0 * builder._
- }
}
+
class InvokerAccount {
String firstName
Set<InvokerTransaction> transactions
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
index f876c89167..218c7292e5 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
@@ -4,6 +4,9 @@ import grails.gorm.annotation.Entity
import grails.gorm.specs.HibernateGormDatastoreSpec
import jakarta.persistence.criteria.JoinType
import org.grails.datastore.mapping.query.Query
+import org.hibernate.Session
+import org.springframework.orm.hibernate5.SessionHolder
+import org.springframework.transaction.support.TransactionSynchronizationManager
import spock.lang.Shared
import java.math.RoundingMode
@@ -573,6 +576,53 @@ class HibernateCriteriaBuilderDirectSpec extends HibernateGormDatastoreSpec {
when: builder.targetClass = DirectTransaction
then: builder.targetClass == DirectTransaction
}
+
+ void "test closeSession unbinds and closes session when not participating"() {
+ given:
+ def sf = manager.hibernateDatastore.sessionFactory
+ // Unbind anything before starting
+ TransactionSynchronizationManager.unbindResourceIfPossible(sf)
+
+ Session nativeSession = sf.openSession()
+ // Builder created without bound resource -> participate = false
+ HibernateCriteriaBuilder b = new HibernateCriteriaBuilder(DirectAccount, sf, manager.hibernateDatastore)
+ // Now bind it so closeSession has something to unbind
+ TransactionSynchronizationManager.unbindResourceIfPossible(sf)
+ TransactionSynchronizationManager.bindResource(sf, new SessionHolder(nativeSession))
+
+ when:
+ b.closeSession()
+
+ then:
+ !TransactionSynchronizationManager.hasResource(sf)
+ !nativeSession.isOpen()
+ }
+
+ void "test closeSession does not unbind or close session when participating"() {
+ given:
+ def sf = manager.hibernateDatastore.sessionFactory
+ // Unbind anything before starting
+ TransactionSynchronizationManager.unbindResourceIfPossible(sf)
+
+ Session nativeSession = sf.openSession()
+ TransactionSynchronizationManager.unbindResourceIfPossible(sf)
+ TransactionSynchronizationManager.bindResource(sf, new SessionHolder(nativeSession))
+ // Builder created with bound resource -> participate = true
+ HibernateCriteriaBuilder b = new HibernateCriteriaBuilder(DirectAccount, sf, manager.hibernateDatastore)
+
+ when:
+ b.closeSession()
+
+ then:
+ TransactionSynchronizationManager.hasResource(sf)
+ nativeSession.isOpen()
+
+ cleanup:
+ TransactionSynchronizationManager.unbindResourceIfPossible(sf)
+ if (nativeSession?.isOpen()) {
+ nativeSession.close()
+ }
+ }
}
@Entity
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
index fa297dea84..f204c0db83 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
@@ -483,6 +483,36 @@ class HibernateCriteriaBuilderSpec extends HibernateGormDatastoreSpec {
cleanup:
results?.close()
}
+
+ void "fetchMode applies joining or selection strategy"() {
+ when:
+ def results = c.list {
+ fetchMode("transactions", org.hibernate.FetchMode.JOIN)
+ eq("firstName", "Fred")
+ }
+ then:
+ results.size() == 1
+ results[0].firstName == "Fred"
+
+ when:
+ results = c.list {
+ fetchMode("transactions", org.hibernate.FetchMode.SELECT)
+ eq("firstName", "Fred")
+ }
+ then:
+ results.size() == 1
+ results[0].firstName == "Fred"
+ }
+
+ void "singleResult returns exactly one row"() {
+ when:
+ c.eq("firstName", "Fred")
+ def result = c.singleResult()
+
+ then:
+ result != null
+ result.firstName == "Fred"
+ }
}
@Entity
diff --git a/grails-doc/src/en/ref/Domain Classes/createCriteria.adoc b/grails-doc/src/en/ref/Domain Classes/createCriteria.adoc
index 49451e7013..7a5a4a98f5 100644
--- a/grails-doc/src/en/ref/Domain Classes/createCriteria.adoc
+++ b/grails-doc/src/en/ref/Domain Classes/createCriteria.adoc
@@ -84,6 +84,7 @@ Method reference:
|Method|Description
|*list*|The default method; returns all matching rows.
|*get*|Returns a unique result, i.e. just one row. The criteria has to be formed that way, that it only queries one row. This method is not to be confused with a limit to just the first row.
+|*singleResult*|Alias for *get*.
|*scroll*|Returns a scrollable result set
|*listDistinct*|If subqueries or associations are used, one may end up with the same row multiple times in the result set. In Hibernate one would do a "CriteriaSpecification.DISTINCT_ROOT_ENTITY". In Grails one can do it by just using this method.
|===
@@ -151,7 +152,10 @@ With dynamic finders, you have access to options such as `max`, `sort`, etc. The
|*order*(String, String)|Specifies both the sort column (the first argument) and the sort order (either 'asc' or 'desc').|`order "age", "desc"`
|*firstResult*(int)|Specifies the offset for the results. A value of 0 will return all records up to the maximum specified.|`firstResult 20`
|*maxResults*(int)|Specifies the maximum number of records to return.|`maxResults 10`
-|*cache*(boolean)|Indicates if the query should be cached (if the query cache is enabled).|`cache 'true'`
+|*cache*(boolean)|Indicates if the query should be cached (if the query cache is enabled).|`cache true`
+|*readOnly*(boolean)|Indicates if the entities returned by the query should be read-only (no dirty checking).|`readOnly true`
+|*lock*(boolean)|Indicates if a pessimistic write lock should be obtained.|`lock true`
+|*fetchMode*(String, FetchMode)|Specifies the fetching strategy for an association.|`fetchMode "transactions", FetchMode.JOIN`
|===
Criteria also support the notion of projections. A projection is used to change the nature of the results. For example the following query uses a projection to count the number of distinct `branch` names that exist for each `Account`: