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`:
