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 fec3c5eaa67ef15740ffbbb52c8bb7b9ea540220 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Wed Mar 4 12:45:52 2026 -0600 refactor(hibernate7): replace dual-field HibernateHqlQuery with HqlQueryDelegate composition --- .../orm/hibernate/query/HibernateHqlQuery.java | 109 +++++++++---------- .../orm/hibernate/query/HqlQueryDelegate.java | 86 +++++++++++++++ .../orm/hibernate/query/MutationQueryDelegate.java | 99 ++++++++++++++++++ .../orm/hibernate/query/SelectQueryDelegate.java | 115 +++++++++++++++++++++ .../hibernate/query/HibernateHqlQuerySpec.groovy | 36 +++++++ 5 files changed, 392 insertions(+), 53 deletions(-) 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 f896a0adf2..02e21230fc 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 @@ -40,33 +40,37 @@ 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.MutationQuery; import org.springframework.context.ApplicationEventPublisher; /** * A query implementation for HQL queries. * + * <p>Hibernate 7 splits query types into {@link org.hibernate.query.Query} (SELECT) and + * {@link org.hibernate.query.MutationQuery} (UPDATE/DELETE), which are siblings under + * {@link org.hibernate.query.CommonQueryContract}. This class uses composition via + * {@link HqlQueryDelegate} to eliminate runtime type-checks and null-field branching. + * * @author Graeme Rocher * @since 6.0 */ @SuppressWarnings("PMD.AvoidDuplicateLiterals") public class HibernateHqlQuery extends Query { - private final org.hibernate.query.Query<?> query; - private final org.hibernate.query.MutationQuery mutationQuery; + /** Handles all query operations; the concrete type encodes whether this is SELECT or UPDATE/DELETE. */ + private final HqlQueryDelegate delegate; + /** Constructs a SELECT query wrapper. */ public HibernateHqlQuery( Session session, PersistentEntity entity, org.hibernate.query.Query<?> query) { super(session, entity); - this.query = query; - this.mutationQuery = null; + this.delegate = new SelectQueryDelegate(query); } + /** Constructs an UPDATE/DELETE query wrapper. */ public HibernateHqlQuery( Session session, PersistentEntity entity, org.hibernate.query.MutationQuery mutationQuery) { super(session, entity); - this.query = null; - this.mutationQuery = mutationQuery; + this.delegate = new MutationQueryDelegate(mutationQuery); } @Override @@ -80,8 +84,8 @@ public class HibernateHqlQuery extends Query { Datastore datastore = getSession().getDatastore(); ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher(); publisher.publishEvent(new PreQueryEvent(datastore, this)); - if (uniqueResult) query.setMaxResults(1); - List results = query.list(); + if (uniqueResult) delegate.setMaxResults(1); + List results = delegate.list(); publisher.publishEvent(new PostQueryEvent(datastore, this, results)); return results; } @@ -89,7 +93,7 @@ public class HibernateHqlQuery extends Query { // ─── Static factory API ────────────────────────────────────────────────── /** - * Session-bound step — creates the {@link org.hibernate.query.Query} from an open {@link + * Session-bound step — creates the appropriate Hibernate query from an open {@link * org.hibernate.Session} and wraps it in a {@link HibernateHqlQuery}. */ protected static HibernateHqlQuery buildQuery( @@ -98,26 +102,24 @@ public class HibernateHqlQuery extends Query { SessionFactory sessionFactory, PersistentEntity entity, HqlQueryContext ctx) { - org.hibernate.query.Query<?> q; + HibernateSession hibernateSession = new HibernateSession(dataStore, sessionFactory); if (StringUtils.isEmpty(ctx.hql())) { - q = session.createQuery("from " + ctx.targetClass().getName(), ctx.targetClass()); - return new HibernateHqlQuery(new HibernateSession(dataStore, sessionFactory), entity, q); + var q = session.createQuery("from " + ctx.targetClass().getName(), ctx.targetClass()); + return new HibernateHqlQuery(hibernateSession, entity, q); } else if (ctx.isUpdate()) { - org.hibernate.query.MutationQuery mq = session.createMutationQuery(ctx.hql()); - HibernateHqlQuery result = - new HibernateHqlQuery(new HibernateSession(dataStore, sessionFactory), entity, mq); + var mq = session.createMutationQuery(ctx.hql()); + var result = new HibernateHqlQuery(hibernateSession, entity, mq); result.setFlushMode(session.getHibernateFlushMode()); return result; } else { - q = + var q = ctx.isNative() ? session.createNativeQuery(ctx.hql(), ctx.targetClass()) : session.createQuery(ctx.hql(), ctx.targetClass()); + var result = new HibernateHqlQuery(hibernateSession, entity, q); + result.setFlushMode(session.getHibernateFlushMode()); + return result; } - HibernateHqlQuery result = - new HibernateHqlQuery(new HibernateSession(dataStore, sessionFactory), entity, q); - result.setFlushMode(session.getHibernateFlushMode()); - return result; } /** @@ -135,8 +137,9 @@ public class HibernateHqlQuery extends Query { GrailsHibernateTemplate template) { HibernateHqlQuery hqlQuery = template.execute(session -> buildQuery(session, dataStore, sessionFactory, entity, ctx)); - if (hqlQuery.getQuery() != null) { - template.applySettings(hqlQuery.getQuery()); + var selectQuery = hqlQuery.selectQuery(); + if (selectQuery != null) { + template.applySettings(selectQuery); } hqlQuery.populateQuerySettings( MapUtils.isNotEmpty(args) ? new HashMap<>(args) : Collections.emptyMap()); @@ -158,30 +161,20 @@ public class HibernateHqlQuery extends Query { } public void populateQuerySettings(Map<?, ?> args) { - if (mutationQuery != null) { - ifPresent(args, DynamicFinder.ARGUMENT_TIMEOUT, v -> mutationQuery.setTimeout(toInt(v))); - ifPresent( - args, - DynamicFinder.ARGUMENT_FLUSH_MODE, - v -> mutationQuery.setQueryFlushMode(GrailsHibernateQueryUtils.convertQueryFlushMode(v))); - return; - } - if (query == null) return; - ifPresent(args, DynamicFinder.ARGUMENT_MAX, v -> query.setMaxResults(toInt(v))); - ifPresent(args, DynamicFinder.ARGUMENT_OFFSET, v -> query.setFirstResult(toInt(v))); - ifPresent(args, DynamicFinder.ARGUMENT_CACHE, v -> query.setCacheable(toBool(v))); - ifPresent(args, DynamicFinder.ARGUMENT_FETCH_SIZE, v -> query.setFetchSize(toInt(v))); - ifPresent(args, DynamicFinder.ARGUMENT_TIMEOUT, v -> query.setTimeout(toInt(v))); - ifPresent(args, DynamicFinder.ARGUMENT_READ_ONLY, v -> query.setReadOnly(toBool(v))); + ifPresent(args, DynamicFinder.ARGUMENT_MAX, v -> delegate.setMaxResults(toInt(v))); + ifPresent(args, DynamicFinder.ARGUMENT_OFFSET, v -> delegate.setFirstResult(toInt(v))); + ifPresent(args, DynamicFinder.ARGUMENT_CACHE, v -> delegate.setCacheable(toBool(v))); + ifPresent(args, DynamicFinder.ARGUMENT_FETCH_SIZE, v -> delegate.setFetchSize(toInt(v))); + ifPresent(args, DynamicFinder.ARGUMENT_TIMEOUT, v -> delegate.setTimeout(toInt(v))); + ifPresent(args, DynamicFinder.ARGUMENT_READ_ONLY, v -> delegate.setReadOnly(toBool(v))); ifPresent( args, DynamicFinder.ARGUMENT_FLUSH_MODE, - v -> query.setQueryFlushMode(GrailsHibernateQueryUtils.convertQueryFlushMode(v))); + v -> delegate.setQueryFlushMode(GrailsHibernateQueryUtils.convertQueryFlushMode(v))); } public void populateQueryWithNamedArguments(Map<?, ?> namedArgs) { if (namedArgs == null) return; - org.hibernate.query.CommonQueryContract target = mutationQuery != null ? mutationQuery : query; namedArgs.forEach( (key, value) -> { if (!(key instanceof CharSequence)) { @@ -189,36 +182,46 @@ public class HibernateHqlQuery extends Query { } String name = key.toString(); if (value == null) { - target.setParameter(name, null); - } else if (mutationQuery == null && value instanceof Collection<?> col) { - query.setParameterList(name, col); - } else if (mutationQuery == null && value.getClass().isArray()) { - query.setParameterList(name, (Object[]) value); + delegate.setParameter(name, null); + } else if (value instanceof Collection<?> col) { + delegate.setParameterList(name, col); + } else if (value.getClass().isArray()) { + delegate.setParameterList(name, (Object[]) value); } else if (value instanceof CharSequence cs) { - target.setParameter(name, cs.toString(), String.class); + delegate.setParameter(name, cs.toString(), String.class); } else { - target.setParameter(name, value); + delegate.setParameter(name, value); } }); } public void populateQueryWithIndexedArguments(List<?> params) { if (params == null) return; - org.hibernate.query.CommonQueryContract target = mutationQuery != null ? mutationQuery : query; for (int i = 0; i < params.size(); i++) { Object val = params.get(i); - if (val instanceof CharSequence cs) target.setParameter(i + 1, cs.toString(), String.class); - else if (val != null) target.setParameter(i + 1, val); - else target.setParameter(i + 1, null); + if (val instanceof CharSequence cs) delegate.setParameter(i + 1, cs.toString(), String.class); + else delegate.setParameter(i + 1, val); } } + /** + * Returns the underlying {@link org.hibernate.query.Query} for SELECT queries, or {@code null} + * for mutation queries. + */ public org.hibernate.query.Query<?> getQuery() { - return query; + return delegate.selectQuery(); + } + + /** + * Returns the underlying {@link org.hibernate.query.Query} for SELECT queries, or {@code null} + * for mutation queries. + */ + public org.hibernate.query.Query<?> selectQuery() { + return delegate.selectQuery(); } public int executeUpdate() { - return mutationQuery != null ? mutationQuery.executeUpdate() : query.executeUpdate(); + return delegate.executeUpdate(); } // ─── Private utilities ──────────────────────────────────────────────────── 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 new file mode 100644 index 0000000000..42e60fe36d --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryDelegate.java @@ -0,0 +1,86 @@ +/* + * 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.Collection; +import java.util.List; +import org.hibernate.query.QueryFlushMode; + +/** + * Abstracts over Hibernate's {@link org.hibernate.query.Query} (SELECT) and + * {@link org.hibernate.query.MutationQuery} (UPDATE/DELETE). The two types are + * siblings under {@link org.hibernate.query.CommonQueryContract} and cannot be held + * in a single typed field, so {@link HibernateHqlQuery} delegates all query + * operations through this interface instead. + * + * <p>Select-only methods ({@link #setMaxResults}, {@link #setCacheable}, etc.) are + * no-ops by default; {@link SelectQueryDelegate} overrides them. Mutation-only + * operations ({@link #executeUpdate}) throw {@link UnsupportedOperationException} + * in {@link SelectQueryDelegate} and vice-versa for {@link #list()} in + * {@link MutationQueryDelegate}. + */ +interface HqlQueryDelegate { + + // ── common ──────────────────────────────────────────────────────────────── + + void setTimeout(int timeout); + + void setQueryFlushMode(QueryFlushMode mode); + + void setParameter(String name, Object value); + + <T> void setParameter(String name, T value, Class<T> type); + + void setParameter(int position, Object value); + + <T> void setParameter(int position, T value, Class<T> type); + + // ── select-only (no-ops for mutation queries) ───────────────────────────── + + default void setMaxResults(int n) {} + + default void setFirstResult(int n) {} + + default void setCacheable(boolean b) {} + + default void setFetchSize(int n) {} + + default void setReadOnly(boolean b) {} + + /** Sets a named collection parameter. For mutation queries, falls back to {@link #setParameter}. */ + default void setParameterList(String name, Collection<?> values) {} + + /** Sets a named array parameter. For mutation queries, falls back to {@link #setParameter}. */ + default void setParameterList(String name, Object[] values) {} + + // ── execution ───────────────────────────────────────────────────────────── + + /** Returns all results. Throws {@link UnsupportedOperationException} for mutation queries. */ + @SuppressWarnings("rawtypes") + List list(); + + /** Executes an UPDATE/DELETE. Throws {@link UnsupportedOperationException} for SELECT queries. */ + int executeUpdate(); + + /** + * Returns the underlying {@link org.hibernate.query.Query} for SELECT queries, or {@code null} + * for mutation queries (used by {@link org.grails.orm.hibernate.GrailsHibernateTemplate#applySettings}). + */ + org.hibernate.query.Query<?> selectQuery(); +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/MutationQueryDelegate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/MutationQueryDelegate.java new file mode 100644 index 0000000000..57554ce374 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/MutationQueryDelegate.java @@ -0,0 +1,99 @@ +/* + * 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.Collection; +import java.util.List; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.QueryFlushMode; + +/** + * {@link HqlQueryDelegate} for HQL UPDATE/DELETE queries backed by + * {@link org.hibernate.query.MutationQuery}. + * + * <p>Select-only methods (setMaxResults, setCacheable, etc.) are inherited as no-ops since + * {@link MutationQuery} does not support them. {@link #setParameterList} falls back to + * {@link #setParameter} with the collection value as best-effort support for IN clauses. + */ +final class MutationQueryDelegate implements HqlQueryDelegate { + + private final MutationQuery mutationQuery; + + MutationQueryDelegate(MutationQuery mutationQuery) { + this.mutationQuery = mutationQuery; + } + + @Override + public void setTimeout(int timeout) { + mutationQuery.setTimeout(timeout); + } + + @Override + public void setQueryFlushMode(QueryFlushMode mode) { + mutationQuery.setQueryFlushMode(mode); + } + + @Override + public void setParameter(String name, Object value) { + mutationQuery.setParameter(name, value); + } + + @Override + public <T> void setParameter(String name, T value, Class<T> type) { + mutationQuery.setParameter(name, value, type); + } + + @Override + public void setParameter(int position, Object value) { + mutationQuery.setParameter(position, value); + } + + @Override + public <T> void setParameter(int position, T value, Class<T> type) { + mutationQuery.setParameter(position, value, type); + } + + @Override + public void setParameterList(String name, Collection<?> values) { + // MutationQuery has no setParameterList; pass collection directly as parameter value + mutationQuery.setParameter(name, values); + } + + @Override + public void setParameterList(String name, Object[] values) { + mutationQuery.setParameter(name, values); + } + + @Override + @SuppressWarnings("rawtypes") + public List list() { + throw new UnsupportedOperationException( + "Mutation query (UPDATE/DELETE) cannot be used for list(); use executeUpdate() instead"); + } + + @Override + public int executeUpdate() { + return mutationQuery.executeUpdate(); + } + + @Override + public org.hibernate.query.Query<?> selectQuery() { + return null; + } +} 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 new file mode 100644 index 0000000000..8b9f9518c8 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectQueryDelegate.java @@ -0,0 +1,115 @@ +/* + * 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.Collection; +import java.util.List; +import org.hibernate.query.QueryFlushMode; + +/** {@link HqlQueryDelegate} for HQL SELECT queries backed by {@link org.hibernate.query.Query}. */ +final class SelectQueryDelegate implements HqlQueryDelegate { + + private final org.hibernate.query.Query<?> query; + + SelectQueryDelegate(org.hibernate.query.Query<?> query) { + this.query = query; + } + + @Override + public void setTimeout(int timeout) { + query.setTimeout(timeout); + } + + @Override + public void setQueryFlushMode(QueryFlushMode mode) { + query.setQueryFlushMode(mode); + } + + @Override + public void setParameter(String name, Object value) { + query.setParameter(name, value); + } + + @Override + public <T> void setParameter(String name, T value, Class<T> type) { + query.setParameter(name, value, type); + } + + @Override + public void setParameter(int position, Object value) { + query.setParameter(position, value); + } + + @Override + public <T> void setParameter(int position, T value, Class<T> type) { + query.setParameter(position, value, type); + } + + @Override + public void setMaxResults(int n) { + query.setMaxResults(n); + } + + @Override + public void setFirstResult(int n) { + query.setFirstResult(n); + } + + @Override + public void setCacheable(boolean b) { + query.setCacheable(b); + } + + @Override + public void setFetchSize(int n) { + query.setFetchSize(n); + } + + @Override + public void setReadOnly(boolean b) { + query.setReadOnly(b); + } + + @Override + public void setParameterList(String name, Collection<?> values) { + query.setParameterList(name, values); + } + + @Override + public void setParameterList(String name, Object[] values) { + query.setParameterList(name, values); + } + + @Override + @SuppressWarnings("rawtypes") + public List list() { + return query.list(); + } + + @Override + public int executeUpdate() { + throw new UnsupportedOperationException( + "SELECT query cannot be used for executeUpdate(); use a MutationQuery instead"); + } + + @Override + public org.hibernate.query.Query<?> selectQuery() { + return query; + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy index 6c8941381a..ab22d31c14 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy @@ -197,6 +197,42 @@ class HibernateHqlQuerySpec extends HibernateGormDatastoreSpec { then: thrown(Exception) } + + // ─── delegate behaviour ───────────────────────────────────────────────── + + void "selectQuery is non-null for SELECT queries"() { + expect: + buildHqlQuery("from HibernateHqlQuerySpecBook").selectQuery() != null + } + + void "selectQuery is null for UPDATE/DELETE queries"() { + expect: + buildHqlQuery("update HibernateHqlQuerySpecBook set pages = 1 where title = :t", + [t: "The Hobbit"], null, [:], true).selectQuery() == null + } + + void "populateQuerySettings silently ignores select-only args for mutation queries"() { + when: "max/offset/cache args passed to an UPDATE query — should not throw" + buildHqlQuery("update HibernateHqlQuerySpecBook set pages = 1 where title = :t", + [t: "The Hobbit"], null, [max: 2, offset: 1, cache: true, fetchSize: 10, readOnly: true], true) + then: + noExceptionThrown() + } + + void "executeUpdate throws UnsupportedOperationException for SELECT query"() { + when: + buildHqlQuery("from HibernateHqlQuerySpecBook").executeUpdate() + then: + thrown(UnsupportedOperationException) + } + + void "list throws UnsupportedOperationException for UPDATE query"() { + when: + buildHqlQuery("update HibernateHqlQuerySpecBook set pages = 1 where title = :t", + [t: "The Hobbit"], null, [:], true).list() + then: + thrown(UnsupportedOperationException) + } } @Entity
