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 <[email protected]>
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`:


Reply via email to