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 2544aea26e76cce351afd5ae304ca9f580d309c3 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Mar 6 12:45:41 2026 -0600 hibernate7: removing GrailsHibernateQueryUtils.java --- .../orm/hibernate/HibernateGormStaticApi.groovy | 196 ++++------- .../hibernate/query/GrailsHibernateQueryUtils.java | 372 --------------------- .../orm/hibernate/query/HibernateHqlQuery.java | 94 +++++- .../grails/orm/hibernate/query/HibernateQuery.java | 2 +- .../orm/hibernate/query/HqlListQueryBuilder.java | 154 +++++++++ .../orm/hibernate/query/HqlQueryDelegate.java | 2 + .../orm/hibernate/query/PagedResultList.java | 67 +++- .../orm/hibernate/query/SelectQueryDelegate.java | 6 + .../query/GrailsHibernateQueryUtilsSpec.groovy | 80 +---- 9 files changed, 371 insertions(+), 602 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy index 49c7e7ad91..d6cad39943 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy @@ -17,25 +17,24 @@ package org.grails.orm.hibernate import org.hibernate.jpa.AvailableHints - import grails.orm.HibernateCriteriaBuilder import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery import jakarta.persistence.criteria.Expression import jakarta.persistence.criteria.Root import org.grails.datastore.gorm.GormEnhancer import org.grails.datastore.gorm.GormStaticApi -import org.grails.datastore.gorm.finders.DynamicFinder import org.grails.datastore.gorm.finders.FinderMethod import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider import org.grails.datastore.mapping.query.api.BuildableCriteria as GrailsCriteria import org.grails.datastore.mapping.query.event.PostQueryEvent import org.grails.datastore.mapping.query.event.PreQueryEvent import org.grails.datastore.mapping.proxy.ProxyHandler -import org.grails.orm.hibernate.query.GrailsHibernateQueryUtils import org.grails.orm.hibernate.query.HibernateHqlQuery -import org.grails.orm.hibernate.query.HqlQueryContext import org.grails.orm.hibernate.query.HibernateQuery +import org.grails.orm.hibernate.query.HqlQueryContext import org.grails.orm.hibernate.query.PagedResultList import org.grails.orm.hibernate.support.HibernateRuntimeUtils @@ -46,8 +45,6 @@ import org.hibernate.query.Query import org.springframework.core.convert.ConversionService import org.springframework.transaction.PlatformTransactionManager -import jakarta.persistence.criteria.CriteriaBuilder -import jakarta.persistence.criteria.CriteriaQuery /** * The implementation of the GORM static method contract for Hibernate @@ -64,7 +61,6 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { protected final HibernateSession hibernateSession protected ProxyHandler proxyHandler protected SessionFactory sessionFactory - protected GrailsHibernateQueryUtils queryUtils protected Class identityType protected ClassLoader classLoader private HibernateGormInstanceApi<D> instanceApi @@ -75,7 +71,6 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { this.hibernateTemplate = new GrailsHibernateTemplate(datastore.getSessionFactory(), datastore) this.conversionService = datastore.mappingContext.conversionService this.proxyHandler = datastore.mappingContext.proxyHandler - this.queryUtils = new GrailsHibernateQueryUtils() this.hibernateSession = new HibernateSession( (HibernateDatastore)datastore, hibernateTemplate.getSessionFactory() @@ -295,7 +290,6 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { } - @Override D findWhere(Map queryMap, Map args) { if (!queryMap) return null @@ -317,21 +311,35 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { doListInternal(query, namedParams, [], args, false) } - @SuppressWarnings('GroovyAssignabilityCheck') - private HibernateHqlQuery prepareHqlQuery(CharSequence hql, boolean isNative, boolean isUpdate, - Map namedParams, Collection positionalParams, Map args) { - def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity) - return HibernateHqlQuery.createHqlQuery( - (HibernateDatastore) datastore, - sessionFactory, - persistentEntity, - ctx, - args, - positionalParams, - getHibernateTemplate() - ) + @Override + List executeQuery(CharSequence query, Collection positionalParams, Map args) { + return doListInternal(query, [:], positionalParams, args, false) + } + + @Override + List<D> findAll(CharSequence query, Collection positionalParams, Map args) { + doListInternal(query, [:], positionalParams, args, false) } + private List<D> getAllInternal(List ids) { + if (!ids) return [] + String idName = persistentEntity.identity.name + String entity = persistentEntity.name + Class<?> idType = persistentEntity.identity.type + List convertedIds = ids.collect { HibernateRuntimeUtils.convertValueToType(it, idType, conversionService) } + List<D> results = doListInternal("from $entity where $idName in (:ids)" as String, [ids: convertedIds], [], [:], false) + Map<Object, D> byId = results.collectEntries { [(it[idName]): it] } + ids.collect { byId[it] } + } + + + @Override + List<D> getAll(Serializable... ids) { + getAllInternal(ids as List) + } + + + private List<D> doListInternal(CharSequence hql, Map namedParams, Collection positionalParams, @@ -357,6 +365,16 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { return (D) sm } + @Override + Integer executeUpdate(CharSequence query, Map params, Map args) { + doInternalExecuteUpdate(query,params,[],args) + } + + @Override + Integer executeUpdate(CharSequence query, Collection indexedParams, Map args) { + doInternalExecuteUpdate(query,[:],indexedParams,args) + } + private Integer doInternalExecuteUpdate(CharSequence hql, Map namedParams, Collection positionalParams, @@ -368,47 +386,22 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { return (Integer) execute } - - - - - @Override - List executeQuery(CharSequence query, Collection positionalParams, Map args) { - return doListInternal(query, [:], positionalParams, args, false) - } - - @Override - List<D> findAll(CharSequence query, Collection positionalParams, Map args) { - doListInternal(query, [:], positionalParams, args, false) - } - - private List<D> getAllInternal(List ids) { - if (!ids) return [] - String idName = persistentEntity.identity.name - String entity = persistentEntity.name - Class<?> idType = persistentEntity.identity.type - List convertedIds = ids.collect { HibernateRuntimeUtils.convertValueToType(it, idType, conversionService) } - List<D> results = doListInternal("from $entity where $idName in (:ids)" as String, [ids: convertedIds], [], [:], false) - Map<Object, D> byId = results.collectEntries { [(it[idName]): it] } - ids.collect { byId[it] } - } - - List<D> getAll(List ids) { - getAllInternal(ids) - } - - List<D> getAll(Long... ids) { - getAllInternal(ids as List) - } - - @Override - List<D> getAll(Serializable... ids) { - getAllInternal(ids as List) + @SuppressWarnings('GroovyAssignabilityCheck') + private HibernateHqlQuery prepareHqlQuery(CharSequence hql, boolean isNative, boolean isUpdate, + Map namedParams, Collection positionalParams, Map args) { + def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity) + return HibernateHqlQuery.createHqlQuery( + (HibernateDatastore) datastore, + sessionFactory, + persistentEntity, + ctx, + args, + positionalParams, + getHibernateTemplate() + ) } - - protected Serializable convertIdentifier(Serializable id) { def identity = persistentEntity.identity if(identity != null) { @@ -435,62 +428,23 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { } - - - - - - @Override List<D> list(Map params = Collections.emptyMap()) { - hibernateTemplate.execute { Session session -> - CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() - CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) - var queryRoot = criteriaQuery.from(persistentEntity.javaClass) - queryUtils.populateArgumentsForCriteria( - persistentEntity, - criteriaQuery, - queryRoot, - criteriaBuilder, - params - , - true - ) - Query query = session.createQuery(criteriaQuery) - - queryUtils.populateArgumentsForCriteria( - persistentEntity, - query, - params, - datastore.mappingContext.conversionService - - ) - - - HibernateHqlQuery hibernateQuery = new HibernateHqlQuery( - new HibernateSession((HibernateDatastore)datastore, sessionFactory), - persistentEntity, - query - ) - hibernateTemplate.applySettings(query) - - params = params ? new HashMap(params) : Collections.emptyMap() - if(params.containsKey(DynamicFinder.ARGUMENT_MAX)) { - criteriaQuery = criteriaBuilder.createQuery(Object.class) - queryRoot = criteriaQuery.from(persistentEntity.javaClass) - return new PagedResultList( - hibernateTemplate, - persistentEntity, - hibernateQuery, - criteriaQuery, - queryRoot, - criteriaBuilder - ) - } - else { - return hibernateQuery.list() - } + firePreQueryEvent() + HibernateHqlQuery hqlQuery = HibernateHqlQuery.forList( + (HibernateDatastore) datastore, + sessionFactory, + persistentEntity, + params, + getHibernateTemplate(), + datastore.mappingContext.conversionService + ) + if (params.containsKey('max')) { + return new PagedResultList(getHibernateTemplate(), persistentEntity, hqlQuery) } + List<D> result = (List<D>) hqlQuery.list() + firePostQueryEvent(result) + result } @Override @@ -510,22 +464,6 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { return builder } - @Override - D lock(Serializable id) { - (D)hibernateTemplate.lock((Class)persistentClass, convertIdentifier(id), LockMode.PESSIMISTIC_WRITE) - } - - @Override - Integer executeUpdate(CharSequence query, Map params, Map args) { - doInternalExecuteUpdate(query,params,[],args) - } - - @Override - Integer executeUpdate(CharSequence query, Collection indexedParams, Map args) { - doInternalExecuteUpdate(query,[:],indexedParams,args) - } - - protected void firePostQueryEvent(Object result) { def hibernateQuery = new HibernateQuery(new HibernateSession((HibernateDatastore) datastore, sessionFactory), persistentEntity) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java deleted file mode 100644 index 29fd4de8b3..0000000000 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.grails.orm.hibernate.query; - -import jakarta.persistence.LockModeType; -import jakarta.persistence.criteria.*; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import org.grails.datastore.mapping.config.Property; -import org.grails.datastore.mapping.model.PersistentEntity; -import org.grails.datastore.mapping.model.PersistentProperty; -import org.grails.datastore.mapping.model.types.Association; -import org.grails.datastore.mapping.model.types.Embedded; -import org.grails.datastore.mapping.reflect.ClassUtils; -import org.grails.orm.hibernate.cfg.Mapping; -import org.grails.orm.hibernate.cfg.MappingCacheHolder; -import org.hibernate.FetchMode; -import org.hibernate.FlushMode; -import org.hibernate.query.Query; -import org.hibernate.query.QueryFlushMode; -import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; -import org.hibernate.query.sqm.tree.expression.SqmFunction; -import org.springframework.core.convert.ConversionService; - -/** - * Utility methods for configuring Hibernate queries - * - * @author Graeme Rocher - * @since 4.0 - */ -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") -public class GrailsHibernateQueryUtils { - - /** - * Populates criteria arguments for the given target class and arguments map - * - * @param entity The {@link PersistentEntity} instance - * @param query The criteria instance - * @param argMap The arguments map - */ - public void populateArgumentsForCriteria( - PersistentEntity entity, - CriteriaQuery<?> query, - Root<?> queryRoot, - CriteriaBuilder criteriaBuilder, - Map<String, Object> argMap, - boolean useDefaultMapping) { - Object fetchObj = argMap.get(HibernateQueryArgument.FETCH.value()); - if (fetchObj instanceof Map<?, ?> fetch) { - for (Object o : fetch.keySet()) { - String associationName = (String) o; - - final FetchMode fetchMode = getFetchMode(fetch.get(associationName)); - if (fetchMode == FetchMode.JOIN) { - queryRoot.join(associationName); - } - } - } - - final Object sortObj = argMap.get(HibernateQueryArgument.SORT.value()); - if (sortObj != null) { - final boolean ignoreCase = - !(argMap.get(HibernateQueryArgument.IGNORE_CASE.value()) instanceof Boolean b) || b; - if (sortObj instanceof Map<?, ?> sortMap) { - for (Object sort : sortMap.keySet()) { - final String order = - HibernateQueryArgument.ORDER_DESC.value().equalsIgnoreCase((String) sortMap.get(sort)) - ? HibernateQueryArgument.ORDER_DESC.value() - : HibernateQueryArgument.ORDER_ASC.value(); - addOrderPossiblyNested( - query, queryRoot, criteriaBuilder, entity, (String) sort, order, ignoreCase); - } - } else { - final String sort = (String) sortObj; - final String order = - HibernateQueryArgument.ORDER_DESC.value().equalsIgnoreCase( - (String) argMap.get(HibernateQueryArgument.ORDER.value())) - ? HibernateQueryArgument.ORDER_DESC.value() - : HibernateQueryArgument.ORDER_ASC.value(); - addOrderPossiblyNested(query, queryRoot, criteriaBuilder, entity, sort, order, ignoreCase); - } - } else if (useDefaultMapping) { - Class<?> theClass = entity.getJavaClass(); - Mapping m = MappingCacheHolder.getInstance().getMapping(theClass); - if (m != null) { - Map<String, String> sortMap = m.getSort().getNamesAndDirections(); - for (Map.Entry<String, String> entry : sortMap.entrySet()) { - String sort = entry.getKey(); - final String order = - HibernateQueryArgument.ORDER_DESC.value().equalsIgnoreCase(entry.getValue()) - ? HibernateQueryArgument.ORDER_DESC.value() - : HibernateQueryArgument.ORDER_ASC.value(); - addOrderPossiblyNested( - query, queryRoot, criteriaBuilder, entity, sort, order, true); - } - } - } - } - - /** - * Populates criteria arguments for the given target class and arguments map - * - * @param entity The {@link PersistentEntity} instance - * @param query The criteria instance - * @param argMap The arguments map - */ - public void populateArgumentsForCriteria( - PersistentEntity entity, - Query<?> query, - Map<String, Object> argMap, - ConversionService conversionService) { - final Integer maxParam = - argMap.containsKey(HibernateQueryArgument.MAX.value()) - ? conversionService.convert(argMap.get(HibernateQueryArgument.MAX.value()), Integer.class) - : null; - final Integer offsetParam = - argMap.containsKey(HibernateQueryArgument.OFFSET.value()) - ? conversionService.convert(argMap.get(HibernateQueryArgument.OFFSET.value()), Integer.class) - : null; - if (argMap.containsKey(HibernateQueryArgument.FETCH_SIZE.value())) { - Integer fetchSize = conversionService.convert(argMap.get(HibernateQueryArgument.FETCH_SIZE.value()), Integer.class); - if (fetchSize != null) { - query.setFetchSize(fetchSize); - } - } - if (argMap.containsKey(HibernateQueryArgument.TIMEOUT.value())) { - Integer timeout = conversionService.convert(argMap.get(HibernateQueryArgument.TIMEOUT.value()), Integer.class); - if (timeout != null) { - query.setTimeout(timeout); - } - } - if (argMap.containsKey(HibernateQueryArgument.FLUSH_MODE.value())) { - query.setQueryFlushMode(convertQueryFlushMode(argMap.get(HibernateQueryArgument.FLUSH_MODE.value()))); - } - if (argMap.containsKey(HibernateQueryArgument.READ_ONLY.value())) { - query.setReadOnly(ClassUtils.getBooleanFromMap(HibernateQueryArgument.READ_ONLY.value(), argMap)); - } - - final int max = maxParam == null ? -1 : maxParam; - final int offset = offsetParam == null ? -1 : offsetParam; - if (max > -1) { - query.setMaxResults(max); - } - if (offset > -1) { - query.setFirstResult(offset); - } - if (ClassUtils.getBooleanFromMap(HibernateQueryArgument.LOCK.value(), argMap)) { - query.setLockMode(LockModeType.PESSIMISTIC_WRITE); - query.setCacheable(false); - } else { - if (argMap.containsKey(HibernateQueryArgument.CACHE.value())) { - query.setCacheable(ClassUtils.getBooleanFromMap(HibernateQueryArgument.CACHE.value(), argMap)); - } else { - cacheCriteriaByMapping(entity.getJavaClass(), query); - } - } - } - - /** - * Add order to criteria, creating necessary subCriteria if nested sort property (ie. - * sort:'nested.property'). - */ - protected void addOrderPossiblyNested( - CriteriaQuery<?> query, - From<?, ?> queryRoot, - CriteriaBuilder criteriaBuilder, - PersistentEntity entity, - String sort, - String order, - boolean ignoreCase) { - int firstDotPos = sort.indexOf("."); - if (firstDotPos == -1) { - final PersistentProperty<? extends Property> property = - (PersistentProperty<? extends Property>) entity.getPropertyByName(sort); - ignoreCase = isIgnoreCaseProperty(ignoreCase, property); - addOrder(entity, query, queryRoot, criteriaBuilder, sort, order, ignoreCase); - } else { // nested property - String sortHead = sort.substring(0, firstDotPos); - String sortTail = sort.substring(firstDotPos + 1); - final PersistentProperty<? extends Property> property = - (PersistentProperty<? extends Property>) entity.getPropertyByName(sortHead); - if (property instanceof Embedded) { - // embedded objects cannot reference entities (at time of writing), so no more recursion - // needed - final PersistentProperty<? extends Property> associatedProperty = - (PersistentProperty<? extends Property>) - ((Embedded<?>) property).getAssociatedEntity().getPropertyByName(sortTail); - ignoreCase = isIgnoreCaseProperty(ignoreCase, associatedProperty); - addOrder(entity, query, queryRoot, criteriaBuilder, sort, order, ignoreCase); - } else if (property instanceof Association<? extends Property> a) { - final Join join = queryRoot.join(sortHead); - PersistentEntity associatedEntity = a.getAssociatedEntity(); - addOrderPossiblyNested( - query, - join, - criteriaBuilder, - associatedEntity, - sortTail, - order, - ignoreCase); // Recurse on nested sort - } - } - } - - protected boolean isIgnoreCaseProperty( - boolean ignoreCase, PersistentProperty<? extends Property> persistentProperty) { - if (ignoreCase && persistentProperty != null && persistentProperty.getType() != String.class) { - ignoreCase = false; - } - return ignoreCase; - } - - /** Add order directly to criteria. */ - protected void addOrder( - PersistentEntity entity, - CriteriaQuery<?> query, - From<?, ?> queryRoot, - CriteriaBuilder criteriaBuilder, - String sort, - String order, - boolean ignoreCase) { - var compositeIdSortOptional = - Optional.ofNullable(entity.getCompositeIdentity()).stream() - .flatMap(Arrays::stream) - .filter(Objects::nonNull) - .filter(id -> sort.equalsIgnoreCase(id.getName())) - .findFirst(); - - if (compositeIdSortOptional.isPresent()) { - Comparator<Order> orderComparator = - new Comparator<>() { - @Override - public int compare(Order o1, Order o2) { - String name1 = getOrderName(o1); - String name2 = getOrderName(o2); - - boolean name1IsSort = name1.equalsIgnoreCase(sort); - boolean name2IsSort = name2.equalsIgnoreCase(sort); - - if (name1IsSort && !name2IsSort) { - return -1; // o1 (the sort property) comes first - } - if (!name1IsSort && name2IsSort) { - return 1; // o2 (the sort property) comes first - } - return 0; // Maintain original order for other properties - } - - private static String getOrderName(Order o1) { - return ((SqmBasicValuedSimplePath<?>) - ((SqmFunction<?>) o1.getExpression()).getArguments().get(0)) - .getNavigablePath() - .getLocalName(); - } - }; - Order[] orders = - Arrays.stream(entity.getCompositeIdentity()) - .map(PersistentProperty::getName) - .map( - name -> - { - Path x = queryRoot.get(name); - return ignoreCase ? criteriaBuilder.upper(x) : queryRoot.get(name); - }) - .map( - path -> - HibernateQueryArgument.ORDER_DESC.value().equals(order) - ? criteriaBuilder.desc(path) - : criteriaBuilder.asc(path)) - .sorted(orderComparator) - .toArray(Order[]::new); - query.orderBy(orders); - - } else if (entity.getIdentity() != null - && sort.equalsIgnoreCase(entity.getIdentity().getName())) { - Expression<?> path = queryRoot; - - if (ignoreCase) { - path = criteriaBuilder.upper((Expression<String>) path); - } - if (HibernateQueryArgument.ORDER_DESC.value().equals(order)) { - query.orderBy(criteriaBuilder.desc(path)); - } else { - query.orderBy(criteriaBuilder.asc(path)); - } - } else { - Expression<?> path = queryRoot.get(sort); - - if (ignoreCase) { - path = criteriaBuilder.upper((Expression<String>) path); - } - if (HibernateQueryArgument.ORDER_DESC.value().equals(order)) { - query.orderBy(criteriaBuilder.desc(path)); - } else { - query.orderBy(criteriaBuilder.asc(path)); - } - } - } - - /** - * Configures the criteria instance to cache based on the configured mapping. - * - * @param targetClass The target class - * @param criteria The criteria - */ - protected void cacheCriteriaByMapping(Class<?> targetClass, Query<?> criteria) { - Mapping m = MappingCacheHolder.getInstance().getMapping(targetClass); - if (m != null && m.getCache() != null && m.getCache().getEnabled()) { - criteria.setCacheable(true); - } - } - - protected static FlushMode convertFlushMode(Object object) { - if (object == null) { - return null; - } - if (object instanceof FlushMode flushMode) { - return flushMode; - } - try { - return FlushMode.valueOf(object.toString()); - } catch (IllegalArgumentException e) { - return FlushMode.COMMIT; - } - } - - public static QueryFlushMode convertQueryFlushMode(Object object) { - FlushMode fm = convertFlushMode(object); - if (fm == null) return QueryFlushMode.DEFAULT; - return switch (fm) { - case ALWAYS -> QueryFlushMode.FLUSH; - case MANUAL, COMMIT -> QueryFlushMode.NO_FLUSH; - default -> QueryFlushMode.DEFAULT; - }; - } - - /** - * Retrieves the fetch mode for the specified instance; otherwise returns the default FetchMode. - * - * @param object The object, converted to a string - * @return The FetchMode - */ - public FetchMode getFetchMode(Object object) { - String name = object != null ? object.toString() : "default"; - if (name.equalsIgnoreCase(FetchMode.JOIN.toString()) || name.equalsIgnoreCase("eager")) { - return FetchMode.JOIN; - } - if (name.equalsIgnoreCase(FetchMode.SELECT.toString()) || name.equalsIgnoreCase("lazy")) { - return FetchMode.SELECT; - } - return FetchMode.DEFAULT; - } -} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java index b912872c29..116715fa02 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java @@ -19,6 +19,7 @@ package org.grails.orm.hibernate.query; import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -39,7 +40,9 @@ import org.grails.orm.hibernate.HibernateSession; import org.grails.orm.hibernate.exceptions.GrailsQueryException; import org.hibernate.FlushMode; import org.hibernate.SessionFactory; +import org.hibernate.query.QueryFlushMode; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.ConversionService; /** * A query implementation for HQL queries. @@ -167,6 +170,31 @@ public class HibernateHqlQuery extends Query { : FlushModeType.COMMIT); } + public void populateQuerySettings(Map<?, ?> args, ConversionService conversionService) { + ifPresent(args, HibernateQueryArgument.MAX.value(), v -> delegate.setMaxResults(toInt(v, conversionService))); + ifPresent(args, HibernateQueryArgument.OFFSET.value(), v -> delegate.setFirstResult(toInt(v, conversionService))); + ifPresent(args, HibernateQueryArgument.CACHE.value(), v -> delegate.setCacheable(toBool(v))); + ifPresent(args, HibernateQueryArgument.FETCH_SIZE.value(), v -> delegate.setFetchSize(toInt(v, conversionService))); + ifPresent(args, HibernateQueryArgument.TIMEOUT.value(), v -> delegate.setTimeout(toInt(v, conversionService))); + ifPresent(args, HibernateQueryArgument.READ_ONLY.value(), v -> delegate.setReadOnly(toBool(v))); + ifPresent(args, HibernateQueryArgument.FLUSH_MODE.value(), v -> delegate.setQueryFlushMode(convertQueryFlushMode(v))); + if (toBoolFromMap(args, HibernateQueryArgument.LOCK.value())) { + delegate.setLockMode(LockModeType.PESSIMISTIC_WRITE); + delegate.setCacheable(false); + } else { + if (!args.containsKey(HibernateQueryArgument.CACHE.value())) { + org.grails.orm.hibernate.cfg.Mapping m = + org.grails.orm.hibernate.cfg.MappingCacheHolder.getInstance() + .getMapping(getEntity().getJavaClass()); + if (m != null && m.getCache() != null && m.getCache().getEnabled()) { + delegate.setCacheable(true); + } + } + } + } + + /** @deprecated Use {@link #populateQuerySettings(Map, ConversionService)} */ + @Deprecated public void populateQuerySettings(Map<?, ?> args) { ifPresent(args, HibernateQueryArgument.MAX.value(), v -> delegate.setMaxResults(toInt(v))); ifPresent(args, HibernateQueryArgument.OFFSET.value(), v -> delegate.setFirstResult(toInt(v))); @@ -174,10 +202,58 @@ public class HibernateHqlQuery extends Query { ifPresent(args, HibernateQueryArgument.FETCH_SIZE.value(), v -> delegate.setFetchSize(toInt(v))); ifPresent(args, HibernateQueryArgument.TIMEOUT.value(), v -> delegate.setTimeout(toInt(v))); ifPresent(args, HibernateQueryArgument.READ_ONLY.value(), v -> delegate.setReadOnly(toBool(v))); - ifPresent( - args, - HibernateQueryArgument.FLUSH_MODE.value(), - v -> delegate.setQueryFlushMode(GrailsHibernateQueryUtils.convertQueryFlushMode(v))); + ifPresent(args, HibernateQueryArgument.FLUSH_MODE.value(), v -> delegate.setQueryFlushMode(convertQueryFlushMode(v))); + } + + /** + * Factory for {@code list(Map params)} — builds HQL with ORDER BY / JOIN FETCH from the params + * map, applies pagination settings, and wraps in a ready-to-execute {@link HibernateHqlQuery}. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static HibernateHqlQuery forList( + HibernateDatastore dataStore, + SessionFactory sessionFactory, + PersistentEntity entity, + Map<?, ?> params, + GrailsHibernateTemplate template, + ConversionService conversionService) { + HqlListQueryBuilder builder = new HqlListQueryBuilder(entity, params); + String hql = builder.buildListHql(); + HqlQueryContext ctx = HqlQueryContext.prepare(hql, false, false, Collections.emptyMap(), entity); + HibernateHqlQuery hqlQuery = + template.execute(session -> buildQuery(session, dataStore, sessionFactory, entity, ctx)); + org.hibernate.query.Query<?> q = hqlQuery.selectQuery(); + if (q != null) template.applySettings(q); + Map<String, Object> mutableParams = params instanceof Map ? new HashMap<>((Map<String, Object>) params) : Collections.emptyMap(); + hqlQuery.populateQuerySettings(mutableParams, conversionService); + return hqlQuery; + } + + /** + * Builds the count HQL string used by {@link PagedResultList} when paging is requested. + */ + public static String buildCountHql(PersistentEntity entity) { + return new HqlListQueryBuilder(entity, Collections.emptyMap()).buildCountHql(); + } + + public static QueryFlushMode convertQueryFlushMode(Object object) { + FlushMode fm = convertFlushMode(object); + if (fm == null) return QueryFlushMode.DEFAULT; + return switch (fm) { + case ALWAYS -> QueryFlushMode.FLUSH; + case MANUAL, COMMIT -> QueryFlushMode.NO_FLUSH; + default -> QueryFlushMode.DEFAULT; + }; + } + + private static FlushMode convertFlushMode(Object object) { + if (object == null) return null; + if (object instanceof FlushMode flushMode) return flushMode; + try { + return FlushMode.valueOf(object.toString()); + } catch (IllegalArgumentException e) { + return FlushMode.COMMIT; + } } public void populateQueryWithNamedArguments(Map<?, ?> namedArgs) { @@ -237,10 +313,20 @@ public class HibernateHqlQuery extends Query { return Integer.parseInt(v.toString()); } + private static int toInt(Object v, ConversionService cs) { + if (v instanceof Integer i) return i; + return cs.convert(v, Integer.class); + } + private static boolean toBool(Object v) { return Boolean.parseBoolean(v.toString()); } + private static boolean toBoolFromMap(Map<?, ?> map, String key) { + Object v = map.get(key); + return v instanceof Boolean b ? b : v != null && Boolean.parseBoolean(v.toString()); + } + private static void ifPresent( Map<?, ?> map, String key, java.util.function.Consumer<Object> action) { Object v = map.get(key); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java index 61a47e48ea..985dc3d2ae 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java @@ -481,7 +481,7 @@ public class HibernateQuery extends Query { } public void setHibernateFlushMode(FlushMode flushMode) { - this.flushMode = GrailsHibernateQueryUtils.convertQueryFlushMode(flushMode); + this.flushMode = HibernateHqlQuery.convertQueryFlushMode(flushMode); } public void setReadOnly(Boolean readOnly) { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java new file mode 100644 index 0000000000..aaf3ce740b --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.grails.datastore.gorm.finders.DynamicFinder; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.orm.hibernate.cfg.Mapping; +import org.grails.orm.hibernate.cfg.MappingCacheHolder; + +/** + * Translates a GORM query-argument map into an HQL string for {@code list()}. + * + * <p>Handles {@code fetch} (JOIN FETCH), {@code sort} / {@code order} / {@code ignoreCase} + * (ORDER BY), and default sort from the entity's {@link Mapping}. The resulting HQL is a plain + * string so it passes through {@link HqlQueryContext} without GString interpolation. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +class HqlListQueryBuilder { + + private final PersistentEntity entity; + private final Map<?, ?> params; + + HqlListQueryBuilder(PersistentEntity entity, Map<?, ?> params) { + this.entity = entity; + this.params = params; + } + + /** Builds the SELECT HQL for the list query (no count). */ + String buildListHql() { + String alias = "e"; + StringBuilder hql = new StringBuilder("from ").append(entity.getName()).append(" ").append(alias); + appendJoinFetch(hql, alias); + appendOrderBy(hql, alias); + return hql.toString(); + } + + /** Builds the scalar count HQL for {@link PagedResultList}. */ + String buildCountHql() { + return "select count(distinct e) from " + entity.getName() + " e"; + } + + // ─── JOIN FETCH ────────────────────────────────────────────────────────── + + private void appendJoinFetch(StringBuilder hql, String alias) { + Object fetchObj = params.get(HibernateQueryArgument.FETCH.value()); + if (!(fetchObj instanceof Map<?, ?> fetchMap)) return; + for (Object key : fetchMap.keySet()) { + String assocName = key.toString(); + Object mode = fetchMap.get(key); + if (isJoinFetch(mode)) { + hql.append(" join fetch ").append(alias).append(".").append(assocName); + } + } + } + + private static boolean isJoinFetch(Object mode) { + if (mode == null) return false; + String s = mode.toString(); + return s.equalsIgnoreCase("join") || s.equalsIgnoreCase("eager"); + } + + // ─── ORDER BY ──────────────────────────────────────────────────────────── + + private void appendOrderBy(StringBuilder hql, String alias) { + List<String> clauses = new ArrayList<>(); + Object sortObj = params.get(HibernateQueryArgument.SORT.value()); + boolean ignoreCase = isIgnoreCase(); + + if (sortObj instanceof Map<?, ?> sortMap) { + for (Object sortKey : sortMap.keySet()) { + String prop = sortKey.toString(); + String dir = direction((String) sortMap.get(sortKey)); + clauses.add(orderClause(alias, prop, dir, ignoreCase && isStringProp(prop))); + } + } else if (sortObj instanceof String sort) { + String dir = direction((String) params.get(HibernateQueryArgument.ORDER.value())); + clauses.add(orderClause(alias, sort, dir, ignoreCase && isStringProp(sort))); + } else { + // fall back to default mapping sort + Mapping m = MappingCacheHolder.getInstance().getMapping(entity.getJavaClass()); + if (m != null) { + m.getSort().getNamesAndDirections().forEach((prop, dir) -> + clauses.add(orderClause(alias, (String) prop, direction((String) dir), isStringProp((String) prop)))); + } + } + + if (!clauses.isEmpty()) { + hql.append(" order by ").append(String.join(", ", clauses)); + } + } + + private String orderClause(String alias, String prop, String dir, boolean upper) { + String path = alias + "." + prop; + return upper ? "upper(" + path + ") " + dir : path + " " + dir; + } + + private static String direction(String raw) { + return HibernateQueryArgument.ORDER_DESC.value().equalsIgnoreCase(raw) + ? HibernateQueryArgument.ORDER_DESC.value() + : HibernateQueryArgument.ORDER_ASC.value(); + } + + private boolean isIgnoreCase() { + Object ic = params.get(HibernateQueryArgument.IGNORE_CASE.value()); + return !(ic instanceof Boolean b) || b; + } + + private boolean isStringProp(String name) { + // handle nested path: only check the leaf property's type + int dot = name.lastIndexOf('.'); + String leaf = dot == -1 ? name : name.substring(dot + 1); + String head = dot == -1 ? null : name.substring(0, dot); + PersistentEntity owner = resolveOwner(head); + if (owner == null) return false; + PersistentProperty<?> prop = owner.getPropertyByName(leaf); + return prop != null && prop.getType() == String.class; + } + + private PersistentEntity resolveOwner(String path) { + if (path == null) return entity; + PersistentProperty<?> prop = entity.getPropertyByName(path); + if (prop instanceof Embedded<?> emb) return emb.getAssociatedEntity(); + if (prop instanceof Association<?> assoc) return assoc.getAssociatedEntity(); + return null; + } + + /** Returns true when the params indicate a paged query (i.e. {@code max} is set). */ + @SuppressWarnings("rawtypes") + static boolean isPaged(Map params) { + return params.containsKey(DynamicFinder.ARGUMENT_MAX); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryDelegate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryDelegate.java index 42e60fe36d..a2e909082a 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryDelegate.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryDelegate.java @@ -63,6 +63,8 @@ interface HqlQueryDelegate { default void setReadOnly(boolean b) {} + default void setLockMode(jakarta.persistence.LockModeType lockModeType) {} + /** Sets a named collection parameter. For mutation queries, falls back to {@link #setParameter}. */ default void setParameterList(String name, Collection<?> values) {} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java index 4eb9e87528..d03c9eb702 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java @@ -32,12 +32,18 @@ public class PagedResultList extends grails.gorm.PagedResultList { private static final long serialVersionUID = 1L; + private final PersistentEntity entity; + private transient GrailsHibernateTemplate hibernateTemplate; + + // CriteriaQuery-based count (legacy path) private final CriteriaQuery criteriaQuery; private final Root queryRoot; private final CriteriaBuilder criteriaBuilder; - private final PersistentEntity entity; - private transient GrailsHibernateTemplate hibernateTemplate; + // HQL-based count (new path) + private final String countHql; + + /** Legacy constructor — count via CriteriaQuery. */ public PagedResultList( GrailsHibernateTemplate template, PersistentEntity entity, @@ -46,11 +52,26 @@ public class PagedResultList extends grails.gorm.PagedResultList { Root queryRoot, CriteriaBuilder criteriaBuilder) { super(hibernateHqlQuery); - hibernateTemplate = template; + this.hibernateTemplate = template; + this.entity = entity; this.criteriaQuery = criteriaQuery; this.queryRoot = queryRoot; this.criteriaBuilder = criteriaBuilder; + this.countHql = null; + } + + /** HQL constructor — count via scalar HQL. */ + public PagedResultList( + GrailsHibernateTemplate template, + PersistentEntity entity, + HibernateHqlQuery hibernateHqlQuery) { + super(hibernateHqlQuery); + this.hibernateTemplate = template; this.entity = entity; + this.countHql = HibernateHqlQuery.buildCountHql(entity); + this.criteriaQuery = null; + this.queryRoot = null; + this.criteriaBuilder = null; } @Override @@ -62,24 +83,36 @@ public class PagedResultList extends grails.gorm.PagedResultList { public int getTotalCount() { if (totalCount == Integer.MIN_VALUE) { totalCount = - hibernateTemplate.execute( - new GrailsHibernateTemplate.HibernateCallback<Integer>() { - public Integer doInHibernate(Session session) - throws HibernateException, SQLException { - final CriteriaQuery finalQuery = - criteriaQuery - .select(criteriaBuilder.count(queryRoot)) - .distinct(true) - .orderBy(); - final Query query = session.createQuery(finalQuery); - hibernateTemplate.applySettings(query); - return ((Number) query.uniqueResult()).intValue(); - } - }); + countHql != null ? countViaHql() : countViaCriteria(); } return totalCount; } + private int countViaHql() { + return hibernateTemplate.execute(session -> { + Query<?> q = session.createQuery(countHql); + hibernateTemplate.applySettings(q); + return ((Number) q.uniqueResult()).intValue(); + }); + } + + private int countViaCriteria() { + return hibernateTemplate.execute( + new GrailsHibernateTemplate.HibernateCallback<Integer>() { + public Integer doInHibernate(Session session) + throws HibernateException, SQLException { + final CriteriaQuery finalQuery = + criteriaQuery + .select(criteriaBuilder.count(queryRoot)) + .distinct(true) + .orderBy(); + final Query query = session.createQuery(finalQuery); + hibernateTemplate.applySettings(query); + return ((Number) query.uniqueResult()).intValue(); + } + }); + } + public void setTotalCount(int totalCount) { this.totalCount = totalCount; } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectQueryDelegate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectQueryDelegate.java index 8b9f9518c8..6f31f7dbaa 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectQueryDelegate.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectQueryDelegate.java @@ -20,6 +20,7 @@ package org.grails.orm.hibernate.query; import java.util.Collection; import java.util.List; +import jakarta.persistence.LockModeType; import org.hibernate.query.QueryFlushMode; /** {@link HqlQueryDelegate} for HQL SELECT queries backed by {@link org.hibernate.query.Query}. */ @@ -86,6 +87,11 @@ final class SelectQueryDelegate implements HqlQueryDelegate { query.setReadOnly(b); } + @Override + public void setLockMode(LockModeType lockModeType) { + query.setLockMode(lockModeType); + } + @Override public void setParameterList(String name, Collection<?> values) { query.setParameterList(name, values); diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtilsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtilsSpec.groovy index dc9284a771..2227581dd1 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtilsSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtilsSpec.groovy @@ -18,22 +18,16 @@ */ package org.grails.orm.hibernate.query -import org.grails.datastore.mapping.model.PersistentEntity -import org.hibernate.FetchMode -import org.hibernate.query.Query import org.hibernate.query.QueryFlushMode -import org.springframework.core.convert.ConversionService import spock.lang.Specification import spock.lang.Unroll class GrailsHibernateQueryUtilsSpec extends Specification { - GrailsHibernateQueryUtils queryUtils = new GrailsHibernateQueryUtils() - @Unroll def "test convertQueryFlushMode for #input"() { expect: - queryUtils.convertQueryFlushMode(input) == expected + HibernateHqlQuery.convertQueryFlushMode(input) == expected where: input | expected @@ -44,76 +38,4 @@ class GrailsHibernateQueryUtilsSpec extends Specification { null | QueryFlushMode.DEFAULT "INVALID" | QueryFlushMode.NO_FLUSH // defaults to COMMIT which is NO_FLUSH } - - @Unroll - def "test getFetchMode for #input"() { - expect: - queryUtils.getFetchMode(input) == expected - - where: - input | expected - "JOIN" | FetchMode.JOIN - "eager" | FetchMode.JOIN - "SELECT" | FetchMode.SELECT - "lazy" | FetchMode.SELECT - "default" | FetchMode.DEFAULT - null | FetchMode.DEFAULT - } - - def "test populateArgumentsForCriteria for Query"() { - given: - PersistentEntity entity = Mock(PersistentEntity) - Query query = Mock(Query) - ConversionService conversionService = Mock(ConversionService) - - Map argMap = [ - max: 10, - offset: 20, - fetchSize: 50, - timeout: 30, - readOnly: true, - cache: true - ] - - entity.getJavaClass() >> Object.class - conversionService.convert(10, Integer.class) >> 10 - conversionService.convert(20, Integer.class) >> 20 - conversionService.convert(50, Integer.class) >> 50 - conversionService.convert(30, Integer.class) >> 30 - - when: - queryUtils.populateArgumentsForCriteria(entity, query, argMap, conversionService) - - then: - 1 * query.setMaxResults(10) - 1 * query.setFirstResult(20) - 1 * query.setFetchSize(50) - 1 * query.setTimeout(30) - 1 * query.setReadOnly(true) - 1 * query.setCacheable(true) - } - - def "test populateArgumentsForCriteria for Query with null conversion results"() { - given: - PersistentEntity entity = Mock(PersistentEntity) - Query query = Mock(Query) - ConversionService conversionService = Mock(ConversionService) - - Map argMap = [ - fetchSize: 50, - timeout: 30 - ] - - entity.getJavaClass() >> Object.class - // Simulate conversion returning null - conversionService.convert(50, Integer.class) >> null - conversionService.convert(30, Integer.class) >> null - - when: - queryUtils.populateArgumentsForCriteria(entity, query, argMap, conversionService) - - then: - 0 * query.setFetchSize(_) - 0 * query.setTimeout(_) - } }
