diff --git a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc index 01e738fc3f8a..e3f87461dd47 100644 --- a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc +++ b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc @@ -31,6 +31,7 @@ include::chapters/caching/Caching.adoc[] include::chapters/events/Events.adoc[] include::chapters/query/hql/Query.adoc[] include::chapters/query/hql/QueryLanguage.adoc[] +include::chapters/query/programmatic/QuerySpecification.adoc[] include::chapters/query/criteria/Criteria.adoc[] include::chapters/query/criteria/CriteriaExtensions.adoc[] include::chapters/query/native/Native.adoc[] diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/programmatic/QuerySpecification.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/programmatic/QuerySpecification.adoc new file mode 100644 index 000000000000..ddde27a385ae --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/programmatic/QuerySpecification.adoc @@ -0,0 +1,125 @@ +[[QuerySpecification]] +== Programmatic Query Specification +:root-project-dir: ../../../../../../../.. + +Hibernate offers an API for creating a representation of a query, adjusting that representation programmatically, and then creating an executable form of the query. The idea is similar in concept to <>, but focused on ease-of-use and less verbosity. + +There is support for both <> and <> queries via the `SelectionSpecification` and `MutationSpecification` contracts, respectively. These can be obtained from both `Session` and `StatelessSession`. + +[NOTE] +==== +These APIs are new in 7.0 and considered incubating. +==== + +[[SelectionSpecification]] +=== SelectionSpecification + +A `SelectionSpecification` allows to iteratively build a query from a "base", adjust the query by adding sorting and restrictions and finally creating an executable <>. We can use HQL as the base - + +.SelectionSpecification from HQL +==== +[source, java, indent=0] +---- +SelectionSpecification spec = session.createSelectionSpecification( + "from Book", + Book.class +); +---- +==== + +or a root entity as the base - + +.SelectionSpecification from root entity +==== +[source, java, indent=0] +---- +SelectionSpecification spec = session.createSelectionSpecification(Book.class); +---- +==== + +Once we have the `SelectionSpecification` we can adjust the query adding restrictions and sorting - + +.Adjusting the SelectionSpecification +==== +[source, java, indent=0] +---- +// from here we can augment the base query "from Book", +// with either restrictions +spec.restriction( + Restriction.restrict( + Book_.suggestedCost, + Range.closed(10.00, 19.99) + ) +); + +// or here with some sorting +spec.order( + Order.asc(Book_.suggestedCost) +) +---- +==== + +[NOTE] +==== +Notice that generally the JPA static metamodel is a convenient and type-safe way to help build these sorting and restriction references. +==== + +After adjusting the query, we can obtain the executable `SelectionQuery`: + +.Using the SelectionSpecification +==== +[source, java, indent=0] +---- +SelectionQuery qry = ds.createQuery(); +List books = qry.getResultList(); +---- +==== + +These calls can even be chained, e.g. + +.Example of chained calls +==== +[source, java, indent=0] +---- +SelectionQuery qry = session.createSelectionSpecification( + "from Book", + Book.class +).restriction( + Restriction.restrict( + Book_.suggestedCost, + Range.closed(10.00, 19.99) + ) +).order( + Order.asc(Book_.suggestedCost) +).createQuery(); +---- +==== + +[NOTE] +==== +We expect, in future releases, to add the ability to handle pagination. + +We also expect to add the ability to use <> references as the base. Possibly even `TypedQueryReference` references. +==== + +[[MutationSpecification]] +=== MutationSpecification + +There is also support for mutation queries through `MutationSpecification`. +At the moment, only update and delete queries are supported. E.g. + +.MutationQuery example +==== +[source, java, indent=0] +---- +MutationQuery qry = session.createMutationSpecification( + "delete Book", + Book.class +).restriction( + Restriction.restrict( + Book_.suggestedCost, + Range.closed(10.00, 19.99) + ) +).createQuery(); +---- +==== diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index 83f8cd0f2128..243a3a38221c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -4,14 +4,23 @@ */ package org.hibernate.engine.spi; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -import java.util.UUID; - +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.FindOption; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.LockOption; +import jakarta.persistence.RefreshOption; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaSelect; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.Metamodel; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.Filter; import org.hibernate.FlushMode; @@ -49,31 +58,23 @@ import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaInsert; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.programmatic.SelectionSpecification; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.QueryProducerImplementor; import org.hibernate.query.sql.spi.NativeQueryImplementor; import org.hibernate.resource.jdbc.spi.JdbcSessionContext; import org.hibernate.resource.transaction.spi.TransactionCoordinator; import org.hibernate.stat.SessionStatistics; - -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.FindOption; -import jakarta.persistence.FlushModeType; -import jakarta.persistence.LockModeType; -import jakarta.persistence.LockOption; -import jakarta.persistence.RefreshOption; -import jakarta.persistence.TypedQueryReference; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.CriteriaSelect; -import jakarta.persistence.criteria.CriteriaUpdate; -import jakarta.persistence.metamodel.Metamodel; -import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.type.format.FormatMapper; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; + /** * A wrapper class that delegates all method invocations to a delegate instance of * {@link SessionImplementor}. This is useful for custom implementations of that @@ -656,6 +657,21 @@ public MutationQuery createNamedMutationQuery(String name) { return delegate.createNamedMutationQuery( name ); } + @Override + public SelectionSpecification createSelectionSpecification(String hql, Class resultType) { + return delegate.createSelectionSpecification( hql, resultType ); + } + + @Override + public SelectionSpecification createSelectionSpecification(Class rootEntityType) { + return delegate.createSelectionSpecification( rootEntityType ); + } + + @Override + public MutationSpecification createMutationSpecification(String hql, Class mutationTarget) { + return delegate.createMutationSpecification( hql, mutationTarget ); + } + @Override public MutationQuery createNativeMutationQuery(String sqlString) { return delegate.createNativeMutationQuery( sqlString ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java index d1aeeca586f8..e1bde24b4bb1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java @@ -4,14 +4,26 @@ */ package org.hibernate.engine.spi; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.ConnectionConsumer; +import jakarta.persistence.ConnectionFunction; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.FindOption; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.LockOption; +import jakarta.persistence.RefreshOption; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaSelect; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.Metamodel; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.Filter; import org.hibernate.FlushMode; @@ -41,25 +53,14 @@ import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaInsert; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.programmatic.SelectionSpecification; import org.hibernate.stat.SessionStatistics; -import jakarta.persistence.ConnectionConsumer; -import jakarta.persistence.ConnectionFunction; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.FindOption; -import jakarta.persistence.FlushModeType; -import jakarta.persistence.LockModeType; -import jakarta.persistence.LockOption; -import jakarta.persistence.RefreshOption; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.TypedQueryReference; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.CriteriaSelect; -import jakarta.persistence.criteria.CriteriaUpdate; -import jakarta.persistence.metamodel.Metamodel; -import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; /** * This helper class allows decorating a Session instance, while the @@ -749,6 +750,21 @@ public MutationQuery createNamedMutationQuery(String name) { return this.lazySession.get().createNamedMutationQuery( name ); } + @Override + public SelectionSpecification createSelectionSpecification(String hql, Class resultType) { + return this.lazySession.get().createSelectionSpecification( hql, resultType ); + } + + @Override + public SelectionSpecification createSelectionSpecification(Class rootEntityType) { + return this.lazySession.get().createSelectionSpecification( rootEntityType ); + } + + @Override + public MutationSpecification createMutationSpecification(String hql, Class mutationTarget) { + return this.lazySession.get().createMutationSpecification( hql, mutationTarget ); + } + @SuppressWarnings("rawtypes") @Override @Deprecated diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 4934be8b4cb8..d5ffc7c3d4f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -10,7 +10,6 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; import org.checkerframework.checker.nullness.qual.Nullable; - import org.hibernate.CacheMode; import org.hibernate.Filter; import org.hibernate.FlushMode; @@ -37,6 +36,8 @@ import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaInsert; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.programmatic.SelectionSpecification; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.QueryProducerImplementor; import org.hibernate.query.sql.spi.NativeQueryImplementor; @@ -228,6 +229,21 @@ public MutationQuery createNamedMutationQuery(String name) { return delegate.createNamedMutationQuery( name ); } + @Override + public SelectionSpecification createSelectionSpecification(String hql, Class resultType) { + return delegate.createSelectionSpecification( hql, resultType ); + } + + @Override + public SelectionSpecification createSelectionSpecification(Class rootEntityType) { + return delegate.createSelectionSpecification( rootEntityType ); + } + + @Override + public MutationSpecification createMutationSpecification(String hql, Class mutationTarget) { + return delegate.createMutationSpecification( hql, mutationTarget ); + } + @Override public MutationQuery createNativeMutationQuery(String sqlString) { return delegate.createNativeMutationQuery( sqlString ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index fa181cabfa96..6281491a786b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -4,19 +4,12 @@ */ package org.hibernate.internal; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serial; -import java.sql.SQLException; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.TimeZone; -import java.util.UUID; -import java.util.function.Function; - import jakarta.persistence.EntityGraph; +import jakarta.persistence.TransactionRequiredException; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.EntityNameResolver; @@ -78,6 +71,10 @@ import org.hibernate.query.hql.spi.SqmQueryImplementor; import org.hibernate.query.named.NamedObjectRepository; import org.hibernate.query.named.NamedResultSetMappingMemento; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.programmatic.SelectionSpecification; +import org.hibernate.query.programmatic.internal.MutationSpecificationImpl; +import org.hibernate.query.programmatic.internal.SelectionSpecificationImpl; import org.hibernate.query.spi.HqlInterpretation; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.sql.internal.NativeQueryImpl; @@ -102,16 +99,22 @@ import org.hibernate.resource.transaction.TransactionRequiredForJoinException; import org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorImpl; import org.hibernate.resource.transaction.spi.TransactionCoordinator; - -import jakarta.persistence.TransactionRequiredException; -import jakarta.persistence.TypedQueryReference; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.CriteriaUpdate; import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.format.FormatMapper; import org.hibernate.type.spi.TypeConfiguration; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.sql.SQLException; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.TimeZone; +import java.util.UUID; +import java.util.function.Function; + import static java.lang.Boolean.TRUE; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.query.sqm.internal.SqmUtil.verifyIsSelectStatement; @@ -1257,6 +1260,21 @@ public MutationQuery createNamedMutationQuery(String queryName) { memento -> createNativeQueryImplementor( queryName, memento ) ); } + @Override + public SelectionSpecification createSelectionSpecification(String hql, Class resultType) { + return new SelectionSpecificationImpl<>( hql, resultType, this ); + } + + @Override + public SelectionSpecification createSelectionSpecification(Class rootEntityType) { + return new SelectionSpecificationImpl<>( rootEntityType, this ); + } + + @Override + public MutationSpecification createMutationSpecification(String hql, Class mutationTarget) { + return new MutationSpecificationImpl<>( hql, mutationTarget, this ); + } + protected NativeQueryImplementor createNativeQueryImplementor(String queryName, NamedNativeQueryMemento memento) { final NativeQueryImplementor query = memento.toQuery( this ); final Boolean isUnequivocallySelect = query.isSelectQuery(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java index 09dff40648c7..2f0ce44aafb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java @@ -5,12 +5,14 @@ package org.hibernate.query; import jakarta.persistence.EntityGraph; -import org.hibernate.query.criteria.JpaCriteriaInsert; - import jakarta.persistence.TypedQueryReference; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; +import org.hibernate.Incubating; +import org.hibernate.query.criteria.JpaCriteriaInsert; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.programmatic.SelectionSpecification; /** * Contract for things that can produce instances of {@link Query} and {@link NativeQuery}. @@ -497,6 +499,59 @@ public interface QueryProducer { */ MutationQuery createNamedMutationQuery(String name); + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain SelectionQuery} based on a base HQL statement, + * allowing the addition of {@linkplain SelectionSpecification#addOrdering sorting} + * and {@linkplain SelectionSpecification#addRestriction restrictions}. + * + * @param hql The base HQL query. + * @param resultType The result type which will ultimately be returned from the {@linkplain SelectionQuery} + * + * @param The root entity type for the query. + * {@code resultType} and {@code } are both expected to refer to a singular query root. + * + * @throws IllegalSelectQueryException The given HQL is expected to be a {@code select} query. This method will + * throw an exception if not. + */ + @Incubating + SelectionSpecification createSelectionSpecification(String hql, Class resultType) + throws IllegalSelectQueryException; + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain SelectionQuery} for the given entity type, + * allowing the addition of {@linkplain SelectionSpecification#addOrdering sorting} + * and {@linkplain SelectionSpecification#addRestriction restrictions}. + * This is effectively the same as calling {@linkplain QueryProducer#createSelectionSpecification(String, Class)} + * with {@code "from {rootEntityType}"} as the HQL. + * + * @param rootEntityType The entity type which is the root of the query. + * + * @param The entity type which is the root of the query. + * {@code resultType} and {@code } are both expected to refer to a singular query root. + */ + @Incubating + SelectionSpecification createSelectionSpecification(Class rootEntityType); + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain MutationQuery} based on a base HQL statement, + * allowing the addition of {@linkplain MutationSpecification#addRestriction restrictions}. + * + * @param hql The base HQL query (expected to be an {@code update} or {@code delete} query). + * @param mutationTarget The entity which is the target of the mutation. + * + * @param The root entity type for the mutation (the "target"). + * {@code mutationTarget} and {@code } are both expected to refer to the mutation target. + * + * @throws IllegalMutationQueryException Only {@code update} and {@code delete} are supported; + * this method will throw an exception if the given HQL query is not an {@code update} or {@code delete}. + */ + @Incubating + MutationSpecification createMutationSpecification(String hql, Class mutationTarget) + throws IllegalMutationQueryException; + /** * Create a {@link Query} instance for the named query. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java index e0c11762f322..5de05004e0ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java @@ -119,6 +119,11 @@ public void validateResultType(Class resultType) { }; } + @Override + public void cacheHqlInterpretation(Object cacheKey, HqlInterpretation hqlInterpretation) { + // nothing to do + } + @Override public ParameterInterpretation resolveNativeQueryParameters( String queryString, diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java index 6cfb9b2d7ebc..0d2b9a3887cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java @@ -124,6 +124,7 @@ public HqlInterpretation resolveHqlInterpretation( if ( statistics.isStatisticsEnabled() ) { statistics.queryPlanCacheHit( queryString ); } + //noinspection unchecked return (HqlInterpretation) existing; } else if ( expectedResultType != null ) { @@ -132,6 +133,7 @@ else if ( expectedResultType != null ) { if ( statistics.isStatisticsEnabled() ) { statistics.queryPlanCacheHit( queryString ); } + //noinspection unchecked return (HqlInterpretation) existingQueryOnly; } } @@ -142,6 +144,11 @@ else if ( expectedResultType != null ) { return hqlInterpretation; } + @Override + public void cacheHqlInterpretation(Object cacheKey, HqlInterpretation hqlInterpretation) { + hqlInterpretationCache.put( cacheKey, hqlInterpretation ); + } + protected static HqlInterpretation createHqlInterpretation( String queryString, Class expectedResultType, @@ -194,38 +201,16 @@ public boolean isEnabled() { @Override public void close() { - // todo (6.0) : clear maps/caches and LOG + log.debug( "Closing QueryInterpretationCache" ); hqlInterpretationCache.clear(); nativeQueryParamCache.clear(); queryPlanCache.clear(); } - private static final class HqlInterpretationCacheKey { - private final String queryString; - private final Class expectedResultType; - - public HqlInterpretationCacheKey(String queryString, Class expectedResultType) { - this.queryString = queryString; - this.expectedResultType = expectedResultType; - } - - @Override - public boolean equals(Object o) { - if ( o.getClass() != HqlInterpretationCacheKey.class ) { - return false; - } - - final HqlInterpretationCacheKey that = (HqlInterpretationCacheKey) o; - return queryString.equals( that.queryString ) - && expectedResultType.equals( that.expectedResultType ); - } - - @Override - public int hashCode() { - int result = queryString.hashCode(); - result = 31 * result + expectedResultType.hashCode(); - return result; - } + /** + * Interpretation-cache key used for HQL interpretations + */ + private record HqlInterpretationCacheKey(String queryString, Class expectedResultType) { } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/MutationSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/MutationSpecification.java new file mode 100644 index 000000000000..e3c6b66a7867 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/MutationSpecification.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.programmatic; + +import jakarta.persistence.criteria.Root; +import org.hibernate.Incubating; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Restriction; + +/** + * Specialization of QuerySpecification for building + * {@linkplain MutationQuery mutation queries}. + * Once all {@linkplain #addRestriction restrictions} are defined, call + * {@linkplain #createQuery()} to obtain the executable form. + * + * @param The entity type which is the target of the mutation. + * + * @author Steve Ebersole + */ +@Incubating +public interface MutationSpecification extends QuerySpecification { + /** + * The entity being mutated. + */ + default Root getMutationTarget() { + return getRoot(); + } + + /** + * Covariant override. + */ + @Override + MutationSpecification addRestriction(Restriction restriction); + + /** + * Finalize the building and create the {@linkplain SelectionQuery} instance. + */ + MutationQuery createQuery(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/QuerySpecification.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/QuerySpecification.java new file mode 100644 index 000000000000..a7fde956cb2c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/QuerySpecification.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.programmatic; + +import jakarta.persistence.criteria.CommonAbstractCriteria; +import jakarta.persistence.criteria.Root; +import org.hibernate.Incubating; +import org.hibernate.query.CommonQueryContract; +import org.hibernate.query.restriction.Restriction; + +/** + * Commonality for all query specifications which allow iterative, + * programmatic building of a query. + * + * @apiNote Query specifications only support a {@linkplain #getRoot() single root}. + * + * @author Steve Ebersole + */ +@Incubating +public interface QuerySpecification { + /** + * Get the root of the query. + * E.g. given the HQL {@code "from Book"}, we have a single {@code Root}. + */ + Root getRoot(); + + /** + * Access to the criteria query which QuerySpecification is + * managing and manipulating internally. + * While it is allowable to directly mutate this tree, users + * should instead prefer to manipulate the tree through the + * methods exposed on the specification itself. + */ + CommonAbstractCriteria getCriteria(); + + /** + * Adds a restriction to the query specification. + * + * @param restriction The restriction predicate to be added. + * + * @return {@code this} for method chaining. + */ + QuerySpecification addRestriction(Restriction restriction); + + /** + * Finalize the building and create executable query instance. + */ + CommonQueryContract createQuery(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/SelectionSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/SelectionSpecification.java new file mode 100644 index 000000000000..969479094826 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/SelectionSpecification.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.programmatic; + +import jakarta.persistence.criteria.CriteriaQuery; +import org.hibernate.Incubating; +import org.hibernate.query.Order; +import org.hibernate.query.QueryProducer; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Restriction; + +import java.util.List; + +/** + * Specialization of QuerySpecification for building + * {@linkplain SelectionQuery selection queries} supporting ordering + * in addition to restrictions. + * Once all {@linkplain #addOrdering sorting} and {@linkplain #addRestriction restrictions} + * are defined, call {@linkplain #createQuery()} to obtain the executable form. + * + * @see QueryProducer#createSelectionSpecification(String, Class) + * + * @author Steve Ebersole + */ +@Incubating +public interface SelectionSpecification extends QuerySpecification { + /** + * Adds an ordering to the selection specification. + * Appended to any previous ordering. + * + * @param order The ordering fragment to be added. + * + * @return {@code this} for method chaining. + */ + SelectionSpecification addOrdering(Order order); + + /** + * Sets the ordering for this selection specification. + * If ordering was already defined, this method drops the previous ordering + * in favor of the passed {@code orders}. + * + * @param order The ordering fragment to be used. + * + * @return {@code this} for method chaining. + */ + SelectionSpecification setOrdering(Order order); + + /** + * Sets the sorting for this selection specification. + * If sorting was already defined, this method drops the previous sorting + * in favor of the passed {@code orders}. + * + * @param orders The sorting fragments to be used. + * + * @return {@code this} for method chaining. + */ + SelectionSpecification setOrdering(List> orders); + + /** + * Covariant override. + */ + @Override + CriteriaQuery getCriteria(); + + /** + * Covariant override. + */ + @Override + SelectionSpecification addRestriction(Restriction restriction); + + /** + * Covariant override. + */ + SelectionQuery createQuery(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/MutationSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/MutationSpecificationImpl.java new file mode 100644 index 000000000000..e3121e1193aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/MutationSpecificationImpl.java @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.programmatic.internal; + +import jakarta.persistence.criteria.CommonAbstractCriteria; +import jakarta.persistence.criteria.Root; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.programmatic.MutationSpecification; +import org.hibernate.query.IllegalMutationQueryException; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.spi.HqlInterpretation; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.SqmQuerySource; +import org.hibernate.query.sqm.internal.QuerySqmImpl; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; + +import java.util.Locale; + +import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; + +/** + * Standard implementation of MutationSpecification + * + * @author Steve Ebersole + */ +public class MutationSpecificationImpl implements MutationSpecification { + private final SharedSessionContractImplementor session; + + private final SqmDeleteOrUpdateStatement sqmStatement; + private final SqmRoot mutationTargetRoot; + + public MutationSpecificationImpl( + String hql, + Class mutationTarget, + SharedSessionContractImplementor session) { + this.session = session; + this.sqmStatement = resolveSqmTree( hql, session ); + this.mutationTargetRoot = resolveSqmRoot( this.sqmStatement, mutationTarget ); + } + + @Override + public Root getRoot() { + return mutationTargetRoot; + } + + @Override + public CommonAbstractCriteria getCriteria() { + return sqmStatement; + } + + @Override + public MutationSpecification addRestriction(Restriction restriction) { + final SqmPredicate sqmPredicate = (SqmPredicate) restriction.toPredicate( + mutationTargetRoot, + sqmStatement.nodeBuilder() + ); + sqmStatement.applyPredicate( sqmPredicate ); + + return this; + } + + @Override + public MutationQuery createQuery() { + return new QuerySqmImpl<>( sqmStatement, true, null, session ); + } + + /** + * Used during construction to parse/interpret the incoming HQL + * and produce the corresponding SQM tree. + */ + private static SqmDeleteOrUpdateStatement resolveSqmTree( + String hql, + SharedSessionContractImplementor session) { + final QueryEngine queryEngine = session.getFactory().getQueryEngine(); + final HqlInterpretation hqlInterpretation = queryEngine + .getInterpretationCache() + .resolveHqlInterpretation( hql, null, queryEngine.getHqlTranslator() ); + + if ( !SqmUtil.isRestrictedMutation( hqlInterpretation.getSqmStatement() ) ) { + throw new IllegalMutationQueryException( "Expecting a delete or update query, but found '" + hql + "'", hql); + } + + // NOTE: this copy is to isolate the actual AST tree from the + // one stored in the interpretation cache + return (SqmDeleteOrUpdateStatement) hqlInterpretation + .getSqmStatement() + .copy( noParamCopyContext( SqmQuerySource.CRITERIA ) ); + } + + /** + * Used during construction. Mainly used to group extracting and + * validating the root. + */ + private static SqmRoot resolveSqmRoot( + SqmDeleteOrUpdateStatement sqmStatement, + Class mutationTarget) { + final SqmRoot mutationTargetRoot = sqmStatement.getTarget(); + if ( mutationTargetRoot.getJavaType() != null + && !mutationTarget.isAssignableFrom( mutationTargetRoot.getJavaType() ) ) { + throw new IllegalArgumentException( + String.format( + Locale.ROOT, + "Mutation target types do not match : %s / %s", + mutationTargetRoot.getJavaType().getName(), + mutationTarget.getName() + ) + ); + } + return mutationTargetRoot; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/SelectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/SelectionSpecificationImpl.java new file mode 100644 index 000000000000..50e55c7daff9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/internal/SelectionSpecificationImpl.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.programmatic.internal; + +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.IllegalSelectQueryException; +import org.hibernate.query.Order; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.programmatic.SelectionSpecification; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.spi.HqlInterpretation; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.SqmQuerySource; +import org.hibernate.query.sqm.internal.SqmSelectionQueryImpl; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSortSpecification; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; + +/** + * Standard implementation of SelectionSpecification + * + * @author Steve Ebersole + */ +public class SelectionSpecificationImpl implements SelectionSpecification { + private final Class resultType; + private final SharedSessionContractImplementor session; + + private final SqmSelectStatement sqmStatement; + private final SqmRoot sqmRoot; + + public SelectionSpecificationImpl( + String hql, + Class resultType, + SharedSessionContractImplementor session) { + this.resultType = resultType; + this.session = session; + this.sqmStatement = resolveSqmTree( hql, resultType, session ); + this.sqmRoot = extractRoot( sqmStatement, resultType, hql ); + } + + public SelectionSpecificationImpl( + Class rootEntityType, + SharedSessionContractImplementor session) { + this( "from " + determineEntityName( rootEntityType, session ), rootEntityType, session ); + } + + @Override + public Root getRoot() { + return sqmRoot; + } + + @Override + public CriteriaQuery getCriteria() { + return sqmStatement; + } + + @Override + public SelectionSpecification addRestriction(Restriction restriction) { + final SqmPredicate sqmPredicate = SqmUtil.restriction( sqmStatement, resultType, restriction ); + sqmStatement.getQuerySpec().applyPredicate( sqmPredicate ); + + return this; + } + + @Override + public SelectionSpecification addOrdering(Order order) { + final SqmSortSpecification sortSpecification = SqmUtil.sortSpecification( sqmStatement, order ); + if ( sqmStatement.getQuerySpec().getOrderByClause() == null ) { + sqmStatement.getQuerySpec().setOrderByClause( new SqmOrderByClause() ); + } + sqmStatement.getQuerySpec().getOrderByClause().addSortSpecification( sortSpecification ); + + return this; + } + + @Override + public final SelectionSpecification setOrdering(Order order) { + sqmStatement.getQuerySpec().setOrderByClause( new SqmOrderByClause() ); + addOrdering( order ); + return this; + } + + @Override + public final SelectionSpecification setOrdering(List> orders) { + sqmStatement.getQuerySpec().setOrderByClause( new SqmOrderByClause() ); + orders.forEach( this::addOrdering ); + return this; + } + + @Override + public SelectionQuery createQuery() { + return new SqmSelectionQueryImpl<>( sqmStatement, true, resultType, session ); + } + + /** + * Used during construction to parse/interpret the incoming HQL + * and produce the corresponding SQM tree. + */ + private static SqmSelectStatement resolveSqmTree( + String hql, + Class resultType, + SharedSessionContractImplementor session) { + final QueryEngine queryEngine = session.getFactory().getQueryEngine(); + final HqlInterpretation hqlInterpretation = queryEngine + .getInterpretationCache() + .resolveHqlInterpretation( hql, resultType, queryEngine.getHqlTranslator() ); + + if ( !SqmUtil.isSelect( hqlInterpretation.getSqmStatement() ) ) { + throw new IllegalSelectQueryException( "Expecting a selection query, but found '" + hql + "'", hql ); + } + hqlInterpretation.validateResultType( resultType ); + + // NOTE: this copy is to isolate the actual AST tree from the + // one stored in the interpretation cache + return (SqmSelectStatement) hqlInterpretation + .getSqmStatement() + .copy( noParamCopyContext( SqmQuerySource.CRITERIA ) ); + } + + /** + * Used during construction. Mainly used to group extracting and + * validating the root. + */ + private SqmRoot extractRoot(SqmSelectStatement sqmStatement, Class resultType, String hql) { + final Set> sqmRoots = sqmStatement.getQuerySpec().getRoots(); + if ( sqmRoots.isEmpty() ) { + throw new QueryException( "Query did not define any roots", hql ); + } + if ( sqmRoots.size() > 1 ) { + throw new QueryException( "Query defined multiple roots", hql ); + } + final SqmRoot sqmRoot = sqmRoots.iterator().next(); + if ( sqmRoot.getJavaType() != null + && !Map.class.isAssignableFrom( sqmRoot.getJavaType() ) + && !resultType.isAssignableFrom( sqmRoot.getJavaType() ) ) { + throw new QueryException( + String.format( + Locale.ROOT, + "Query root [%s] and result type [%s] are not compatible", + sqmRoot.getJavaType().getName(), + resultType.getName() + ), + hql + ); + } + //noinspection unchecked + return (SqmRoot) sqmRoot; + } + + private static String determineEntityName( + Class rootEntityType, + SharedSessionContractImplementor session) { + final EntityDomainType entityType = session + .getFactory() + .getJpaMetamodel() + .findEntityType( rootEntityType ); + if ( entityType == null ) { + return rootEntityType.getName(); + } + return entityType.getName(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/programmatic/package-info.java b/hibernate-core/src/main/java/org/hibernate/query/programmatic/package-info.java new file mode 100644 index 000000000000..46d6d4a84f62 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/programmatic/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ + +/** + * Support for {@linkplain org.hibernate.query.programmatic.SelectionSpecification} + * and {@linkplain org.hibernate.query.programmatic.MutationSpecification}. + * + * @author Steve Ebersole + */ +package org.hibernate.query.programmatic; diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryInterpretationCache.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryInterpretationCache.java index f0f99068313a..65fe6accbafb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryInterpretationCache.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryInterpretationCache.java @@ -35,6 +35,7 @@ default Key prepareForStore() { int getNumberOfCachedQueryPlans(); HqlInterpretation resolveHqlInterpretation(String queryString, Class expectedResultType, HqlTranslator translator); + void cacheHqlInterpretation(Object cacheKey, HqlInterpretation hqlInterpretation); SelectQueryPlan resolveSelectQueryPlan(Key key, Supplier> creator); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 88e8ec1c3a9c..0bd5a14c33dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -4,20 +4,16 @@ */ package org.hibernate.query.sqm.internal; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; import jakarta.persistence.EntityGraph; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.Parameter; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.TemporalType; import org.hibernate.CacheMode; import org.hibernate.FlushMode; -import org.hibernate.query.QueryFlushMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -41,8 +37,8 @@ import org.hibernate.query.Order; import org.hibernate.query.Page; import org.hibernate.query.Query; +import org.hibernate.query.QueryFlushMode; import org.hibernate.query.QueryParameter; -import org.hibernate.query.restriction.Restriction; import org.hibernate.query.ResultListTransformer; import org.hibernate.query.TupleTransformer; import org.hibernate.query.criteria.internal.NamedCriteriaQueryMementoImpl; @@ -51,6 +47,7 @@ import org.hibernate.query.hql.spi.SqmQueryImplementor; import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; +import org.hibernate.query.restriction.Restriction; import org.hibernate.query.spi.DelegatingQueryOptions; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.spi.HqlInterpretation; @@ -81,16 +78,18 @@ import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.ListResultsConsumer; - -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; -import jakarta.persistence.FlushModeType; -import jakarta.persistence.LockModeType; -import jakarta.persistence.Parameter; -import jakarta.persistence.PersistenceException; -import jakarta.persistence.TemporalType; import org.hibernate.sql.results.spi.SingleResultConsumer; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + import static java.util.Collections.emptyMap; import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE; @@ -197,9 +196,20 @@ public QuerySqmImpl( SqmStatement criteria, Class expectedResultType, SharedSessionContractImplementor producer) { + this( criteria, producer.isCriteriaCopyTreeEnabled(), expectedResultType, producer ); + } + + /** + * Used from {@linkplain org.hibernate.query.QueryProducer#createMutationSpecification} + */ + public QuerySqmImpl( + SqmStatement criteria, + boolean copyAst, + Class expectedResultType, + SharedSessionContractImplementor producer) { super( producer ); hql = CRITERIA_HQL_STRING; - if ( producer.isCriteriaCopyTreeEnabled() ) { + if ( copyAst ) { sqm = criteria.copy( SqmCopyContext.simpleContext() ); if ( producer.isCriteriaPlanCacheEnabled() ) { queryStringCacheKey = sqm.toHqlString(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index 1769485fc24e..07f92fd578d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -162,10 +162,18 @@ public SqmSelectionQueryImpl( SqmSelectStatement criteria, Class expectedResultType, SharedSessionContractImplementor session) { + this( criteria, session.isCriteriaCopyTreeEnabled(), expectedResultType, session ); + } + + public SqmSelectionQueryImpl( + SqmSelectStatement criteria, + boolean copyAst, + Class expectedResultType, + SharedSessionContractImplementor session) { super( session ); this.expectedResultType = expectedResultType; hql = CRITERIA_HQL_STRING; - if ( session.isCriteriaCopyTreeEnabled() ) { + if ( copyAst ) { sqm = criteria.copy( SqmCopyContext.simpleContext() ); if ( session.isCriteriaPlanCacheEnabled() ) { queryStringCacheKey = sqm.toHqlString(); @@ -212,6 +220,7 @@ public SqmSelectionQueryImpl( setComment( hql ); tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); + } SqmSelectionQueryImpl(AbstractSqmSelectionQuery original, KeyedPage keyedPage) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index 26572aa7de10..c1f307958e52 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -43,12 +43,14 @@ import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.query.IllegalMutationQueryException; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.Order; import org.hibernate.query.QueryTypeMismatchException; -import org.hibernate.query.criteria.JpaOrder; +import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.restriction.Restriction; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterImplementor; @@ -59,6 +61,7 @@ import org.hibernate.query.sqm.spi.JdbcParameterBySqmParameterAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.SqmDmlStatement; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmStatement; @@ -74,6 +77,7 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.predicate.SqmWhereClause; import org.hibernate.query.sqm.tree.select.SqmOrderByClause; import org.hibernate.query.sqm.tree.select.SqmQueryGroup; @@ -130,6 +134,10 @@ public static boolean isMutation(SqmStatement sqm) { return sqm instanceof SqmDmlStatement; } + public static boolean isRestrictedMutation(SqmStatement sqmStatement) { + return sqmStatement instanceof SqmDeleteOrUpdateStatement; + } + public static void verifyIsSelectStatement(SqmStatement sqm, String hqlString) { if ( ! isSelect( sqm ) ) { throw new IllegalSelectQueryException( @@ -163,6 +171,20 @@ public static IllegalQueryOperationException expectingNonSelect(SqmStatement ); } + public static void verifyIsRestrictedMutation(SqmStatement sqm, String hqlString) { + if ( ! isRestrictedMutation( sqm ) ) { + throw new IllegalMutationQueryException( + String.format( + Locale.ROOT, + "Expecting a restricted mutation query [%s], but found %s", + SqmDeleteOrUpdateStatement.class.getName(), + sqm.getClass().getName() + ), + hqlString + ); + } + } + public static @Nullable String determineAffectedTableName(TableGroup tableGroup, ValuedModelPart mapping) { return tableGroup.getModelPart() instanceof EntityAssociationMapping associationMapping && !associationMapping.containsTableReference( mapping.getContainingTableExpression() ) @@ -841,7 +863,7 @@ public Map, SqmJpaCriteriaParameterWrapper> getJpaCri } } - static JpaOrder sortSpecification(SqmSelectStatement sqm, Order order) { + public static SqmSortSpecification sortSpecification(SqmSelectStatement sqm, Order order) { final List> items = sqm.getQuerySpec().getSelectClause().getSelectionItems(); final int element = order.getElement(); if ( element < 1) { @@ -922,6 +944,15 @@ public static Class resolveExpressibleJavaTypeClass(final SqmExpression ex : expressible.getExpressibleJavaType().getJavaTypeClass(); } + public static SqmPredicate restriction( + SqmSelectStatement sqmStatement, + Class resultType, + Restriction restriction) { + //noinspection unchecked + final JpaRoot root = (JpaRoot) sqmStatement.getRoot( 0, resultType ); + return (SqmPredicate) restriction.toPredicate( root, sqmStatement.nodeBuilder() ); + } + private static class CriteriaParameterCollector { private Set> sqmParameters; private Map, List>> jpaCriteriaParamResolutions; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java index 2036817c5757..ecedc7b2711a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java @@ -4,12 +4,8 @@ */ package org.hibernate.query.sqm.tree; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - +import jakarta.persistence.criteria.AbstractQuery; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.sqm.NodeBuilder; @@ -22,8 +18,11 @@ import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.query.sqm.tree.select.SqmSubQuery; -import jakarta.persistence.criteria.AbstractQuery; -import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; /** * @author Steve Ebersole diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java new file mode 100644 index 000000000000..77b51d54b369 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.dynamic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "BasicEntity") +@Table(name = "BasicEntity") +public class BasicEntity { + @Id + private Integer id; + private String name; + private int position; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SimpleQuerySpecificationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SimpleQuerySpecificationTests.java new file mode 100644 index 000000000000..5461684cf87f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SimpleQuerySpecificationTests.java @@ -0,0 +1,211 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.dynamic; + +import org.hibernate.query.IllegalMutationQueryException; +import org.hibernate.query.IllegalSelectQueryException; +import org.hibernate.query.Order; +import org.hibernate.query.range.Range; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = BasicEntity.class) +@SessionFactory(useCollectingStatementInspector = true) +public class SimpleQuerySpecificationTests { + @Test + void testSimpleSelectionOrder(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .addOrdering( Order.asc( BasicEntity_.position ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position" ); + } + + @Test + void testSimpleSelectionOrderMultiple(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .addOrdering( Order.asc( BasicEntity_.position ) ) + .addOrdering( Order.asc( BasicEntity_.id ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position,be1_0.id" ); + } + + @Test + void testSimpleSelectionSetOrdering(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .setOrdering( Order.asc( BasicEntity_.position ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position" ); + } + + @Test + void testSimpleSelectionSetOrderingMultiple(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .setOrdering( List.of( Order.asc( BasicEntity_.position ), Order.asc( BasicEntity_.id ) ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position,be1_0.id" ); + } + + @Test + void testSimpleSelectionSetOrderingReplace(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .setOrdering( Order.asc( BasicEntity_.id ) ) + .setOrdering( Order.asc( BasicEntity_.position ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position" ); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .addOrdering( Order.asc( BasicEntity_.id ) ) + .setOrdering( Order.asc( BasicEntity_.position ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position" ); + } + + @Test + void testSimpleSelectionRestriction(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity", BasicEntity.class ) + .addRestriction( Restriction.restrict( BasicEntity_.position, Range.closed( 1, 5 ) ) ) + .createQuery() + .list(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " where be1_0.position between ? and ?" ); + } + + @Test + void testSimpleMutationRestriction(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createMutationSpecification( "delete BasicEntity", BasicEntity.class ) + .addRestriction( Restriction.restrict( BasicEntity_.position, Range.closed( 1, 5 ) ) ) + .createQuery() + .executeUpdate(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " where be1_0.position between ? and ?" ); + } + + @Test + void testRootEntityForm(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( BasicEntity.class ) + .addOrdering( Order.asc( BasicEntity_.position ) ) + .createQuery() + .getResultList(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " order by be1_0.position" ); + } + + @Test + void testBaseParameters(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + session.createSelectionSpecification( "from BasicEntity where id > :id", BasicEntity.class ) + .addRestriction( Restriction.restrict( BasicEntity_.position, Range.closed( 1, 5 ) ) ) + .createQuery() + .setParameter( "id", 200 ) + .getResultList(); + } ); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " where be1_0.id>? and be1_0.position between ? and ?" ); + } + + @Test + void testIllegalSelection(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + try { + session.createSelectionSpecification( "delete BasicEntity", BasicEntity.class ); + fail( "Expecting a IllegalSelectQueryException, but not thrown" ); + } + catch (IllegalSelectQueryException expected) { + } + } ); + } + + @Test + void testIllegalMutation(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + try { + session.createMutationSpecification( "from BasicEntity", BasicEntity.class ); + fail( "Expecting a IllegalMutationQueryException, but not thrown" ); + } + catch (IllegalMutationQueryException expected) { + } + } ); + } +} diff --git a/whats-new.adoc b/whats-new.adoc index be6a818759c0..65702ef4d153 100644 --- a/whats-new.adoc +++ b/whats-new.adoc @@ -52,7 +52,6 @@ The https://github.com/hibernate/hibernate-models[Hibernate Models] project was to HCANN. 7.0 uses Hibernate Models in place of HCANN. - [[soft-delete-timestamp]] == @SoftDelete with TIMESTAMP @@ -122,6 +121,53 @@ The new operation `Session.findMultiple()` provides a convenient way to fetch a Combined with the `BatchSize` option, allows breaking up the JDBC calls into "batches". +[[QuerySpecification]] +== QuerySpecification + +A new API has been added for incremental definition of a query, with support for selections - + +==== +[source, java, indent=0] +---- +SelectionQuery qry = session.createSelectionSpecification( + "from Book", + Book.class +).restriction( + Restriction.restrict( + Book_.suggestedCost, + Range.closed(10.00, 19.99) + ) +).order( + Order.asc(Book_.suggestedCost) +).createQuery(); +---- +==== + +as well as mutations - + +==== +[source, java, indent=0] +---- +MutationQuery qry = session.createMutationSpecification( + "delete Book", + Book.class +).restriction( + Restriction.restrict( + Book_.suggestedCost, + Range.closed(10.00, 19.99) + ) +).createQuery(); +---- +==== + +[NOTE] +==== +These APIs are considered incubating. +==== + +See the link:{userGuideBase}#QuerySpecification[User Guide] for details. + + [[session-managed-entities]] == Direct access to first-level cache