From 39de0115f79bcf961ee40a91f2570c8946900415 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 18 Jul 2024 15:23:40 -0500 Subject: [PATCH] HHH-18306 - Implicit instantiation for queries with single selection item broken HHH-18401 - SelectionQuery needs better validation of query return type --- .../AbstractSharedSessionContract.java | 7 +- .../org/hibernate/query/SelectionQuery.java | 11 - .../query/spi/AbstractSelectionQuery.java | 209 ++++++++++++------ .../org/hibernate/query/spi/QueryEngine.java | 4 + .../sqm/internal/SqmSelectionQueryImpl.java | 4 +- .../results/BasicCriteriaResultTests.java | 2 +- .../query/results/BasicHqlResultTests.java | 2 +- .../hibernate/orm/test/query/results/Dto.java | 31 +++ .../orm/test/query/results/Dto2.java | 25 +++ .../results/ImplicitInstantiationTests.java | 140 ++++++++++++ .../query/results/InvalidReturnTests.java | 147 ++++++++++++ .../orm/test/query/results/Queries.java | 40 ++++ .../orm/test/query/results/SimpleEntity.java | 13 +- .../results/TypedQueryCreationTests.java | 202 +++++++++++++++++ 14 files changed, 742 insertions(+), 95 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto2.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitInstantiationTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/InvalidReturnTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Queries.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/results/TypedQueryCreationTests.java 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 08a033f3f1..3a3e80c088 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -828,12 +828,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont protected HqlInterpretation interpretHql(String hql, Class resultType) { final QueryEngine queryEngine = getFactory().getQueryEngine(); - return queryEngine.getInterpretationCache() - .resolveHqlInterpretation( - hql, - resultType, - queryEngine.getHqlTranslator() - ); + return queryEngine.interpretHql( hql, resultType ); } protected static void checkSelectionQuery(String hql, HqlInterpretation hqlInterpretation) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java index 455d5d5a40..9f65a9b265 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java @@ -407,17 +407,6 @@ public interface SelectionQuery extends CommonQueryContract { */ SelectionQuery setFirstResult(int startPosition); -// /** -// * Set the page of results to return. -// * -// * @param pageNumber the page to return, where pages are numbered from zero -// * @param pageSize the number of results per page -// * -// * @since 6.3 -// */ -// @Incubating -// SelectionQuery setPage(int pageSize, int pageNumber); - /** * Set the {@linkplain Page page} of results to return. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index 620f837c53..6aabdc020b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -19,19 +19,6 @@ import java.util.Spliterator; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.FlushModeType; -import jakarta.persistence.LockModeType; -import jakarta.persistence.NoResultException; -import jakarta.persistence.Parameter; -import jakarta.persistence.TemporalType; -import jakarta.persistence.Tuple; -import jakarta.persistence.TupleElement; -import jakarta.persistence.criteria.CompoundSelection; -import org.checkerframework.checker.nullness.qual.Nullable; - import org.hibernate.CacheMode; import org.hibernate.FlushMode; import org.hibernate.HibernateException; @@ -48,6 +35,10 @@ import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.jpa.internal.util.LockModeTypeHelper; import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.IdentifiableDomainType; +import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.query.BindableType; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.QueryParameter; @@ -77,6 +68,19 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Parameter; +import jakarta.persistence.TemporalType; +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; +import jakarta.persistence.criteria.CompoundSelection; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Spliterators.spliteratorUnknownSize; import static org.hibernate.CacheMode.fromJpaModes; import static org.hibernate.FlushMode.fromJpaFlushMode; @@ -84,12 +88,12 @@ import static org.hibernate.cfg.AvailableSettings.JAKARTA_SHARED_CACHE_RETRIEVE_ import static org.hibernate.cfg.AvailableSettings.JAKARTA_SHARED_CACHE_STORE_MODE; import static org.hibernate.cfg.AvailableSettings.JPA_SHARED_CACHE_RETRIEVE_MODE; import static org.hibernate.cfg.AvailableSettings.JPA_SHARED_CACHE_STORE_MODE; -import static org.hibernate.jpa.QueryHints.HINT_CACHEABLE; -import static org.hibernate.jpa.QueryHints.HINT_CACHE_MODE; -import static org.hibernate.jpa.QueryHints.HINT_CACHE_REGION; -import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE; -import static org.hibernate.jpa.QueryHints.HINT_FOLLOW_ON_LOCKING; -import static org.hibernate.jpa.QueryHints.HINT_READONLY; +import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; +import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE; +import static org.hibernate.jpa.HibernateHints.HINT_CACHE_REGION; +import static org.hibernate.jpa.HibernateHints.HINT_FETCH_SIZE; +import static org.hibernate.jpa.HibernateHints.HINT_FOLLOW_ON_LOCKING; +import static org.hibernate.jpa.HibernateHints.HINT_READ_ONLY; import static org.hibernate.query.sqm.internal.SqmUtil.isHqlTuple; import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; @@ -255,7 +259,8 @@ public abstract class AbstractSelectionQuery protected abstract String getQueryString(); /** - * Used during handling of Criteria queries + * Used to validate that the specified query return type is valid (i.e. the user + * did not pass {@code Integer.class} when the selection is an entity) */ protected void visitQueryReturnType( SqmQueryPart queryPart, @@ -298,53 +303,77 @@ public abstract class AbstractSelectionQuery SqmQuerySpec querySpec, Class expectedResultClass, SessionFactoryImplementor sessionFactory) { - if ( !isResultTypeAlwaysAllowed( expectedResultClass ) ) { - final List> selections = querySpec.getSelectClause().getSelections(); - if ( selections.size() == 1 ) { - // we have one item in the select list, - // the type has to match (no instantiation) - final SqmSelection sqmSelection = selections.get( 0 ); - final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode(); - if ( selectableNode.isCompoundSelection() ) { - final Class expectedSelectItemType = expectedResultClass.isArray() - ? expectedResultClass.getComponentType() - : expectedResultClass; - for ( JpaSelection selection : selectableNode.getSelectionItems() ) { - verifySelectionType( expectedSelectItemType, sessionFactory, (SqmSelectableNode) selection ); - } - } - else { - verifySelectionType( expectedResultClass, sessionFactory, sqmSelection.getSelectableNode() ); + if ( isResultTypeAlwaysAllowed( expectedResultClass ) ) { + // the result-class is always safe to use (Object, ...) + return; + } + + final List> selections = querySpec.getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + final SqmSelection sqmSelection = selections.get( 0 ); + final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode(); + if ( selectableNode.isCompoundSelection() ) { + final Class expectedSelectItemType = expectedResultClass.isArray() + ? expectedResultClass.getComponentType() + : expectedResultClass; + for ( JpaSelection selection : selectableNode.getSelectionItems() ) { + verifySelectionType( expectedSelectItemType, sessionFactory, (SqmSelectableNode) selection ); } } - else if ( expectedResultClass.isArray() ) { - final Class componentType = expectedResultClass.getComponentType(); - for ( SqmSelection selection : selections ) { - verifySelectionType( componentType, sessionFactory, selection.getSelectableNode() ); - } + else { + verifySingularSelectionType( expectedResultClass, sessionFactory, sqmSelection ); + } + } + else if ( expectedResultClass.isArray() ) { + final Class componentType = expectedResultClass.getComponentType(); + for ( SqmSelection selection : selections ) { + verifySelectionType( componentType, sessionFactory, selection.getSelectableNode() ); } - // else, let's assume we can instantiate it! } } - private static void verifySelectionType( + /** + * Special case for a single, non-compound selection-item. It is essentially + * a special case of {@linkplain #verifySelectionType} which additionally + * handles the case where the type of the selection-item can be used to + * instantiate the result-class (result-class has a matching constructor). + * + * @apiNote We don't want to hoist this into {@linkplain #verifySelectionType} + * itself because this can only happen for the root non-compound case, and we + * want to avoid the try/catch otherwise + */ + private static void verifySingularSelectionType( Class expectedResultClass, SessionFactoryImplementor sessionFactory, SqmSelection sqmSelection) { - // special case for parameters in the select list - final SqmSelectableNode selection = sqmSelection.getSelectableNode(); - if ( selection instanceof SqmParameter ) { - final SqmParameter sqmParameter = (SqmParameter) selection; - final SqmExpressible nodeType = sqmParameter.getNodeType(); - // we may not yet know a selection type - if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { - // we can't verify the result type up front - return; + final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode(); + try { + verifySelectionType( expectedResultClass, sessionFactory, selectableNode ); + } + catch (QueryTypeMismatchException mismatchException) { + // Check for special case of a single selection item and implicit instantiation. + // See if the selected type can be used to instantiate the expected-type + final JavaType javaTypeDescriptor = selectableNode.getJavaTypeDescriptor(); + if ( javaTypeDescriptor != null ) { + final Class selectedJavaType = javaTypeDescriptor.getJavaTypeClass(); + // ignore the exception if the expected type has a constructor accepting the selected item type + if ( hasMatchingConstructor( expectedResultClass, selectedJavaType ) ) { + // ignore it + } + else { + throw mismatchException; + } } } + } - if ( !sessionFactory.getSessionFactoryOptions().getJpaCompliance().isJpaQueryComplianceEnabled() ) { - verifyResultType( expectedResultClass, selection.getExpressible() ); + private static boolean hasMatchingConstructor(Class expectedResultClass, Class selectedJavaType) { + try { + expectedResultClass.getDeclaredConstructor( selectedJavaType ); + return true; + } + catch (NoSuchMethodException e) { + return false; } } @@ -384,24 +413,58 @@ public abstract class AbstractSelectionQuery || expectedResultClass == Tuple.class; } - protected static void verifyResultType(Class resultClass, @Nullable SqmExpressible sqmExpressible) { - if ( sqmExpressible != null ) { - final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); - assert expressibleJavaType != null; - final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); - if ( javaTypeClass != Object.class && !resultClass.isAssignableFrom( javaTypeClass ) ) { - if ( expressibleJavaType instanceof PrimitiveJavaType ) { - final PrimitiveJavaType javaType = (PrimitiveJavaType) expressibleJavaType; - if ( javaType.getPrimitiveClass() != resultClass ) { - throwQueryTypeMismatchException( resultClass, sqmExpressible ); - } - } - else if ( !isMatchingDateType( javaTypeClass, resultClass, sqmExpressible ) ) { - throwQueryTypeMismatchException( resultClass, sqmExpressible ); - } - // else special case, we are good - } + protected static void verifyResultType(Class resultClass, @Nullable SqmExpressible selectionExpressible) { + if ( selectionExpressible == null ) { + // nothing we can validate + return; } + + final JavaType selectionExpressibleJavaType = selectionExpressible.getExpressibleJavaType(); + assert selectionExpressibleJavaType != null; + + final Class selectionExpressibleJavaTypeClass = selectionExpressibleJavaType.getJavaTypeClass(); + if ( selectionExpressibleJavaTypeClass == Object.class ) { + + } + if ( selectionExpressibleJavaTypeClass != Object.class ) { + // performs a series of opt-out checks for validity... each if branch and return indicates a valid case + if ( resultClass.isAssignableFrom( selectionExpressibleJavaTypeClass ) ) { + return; + } + + if ( selectionExpressibleJavaType instanceof PrimitiveJavaType ) { + final PrimitiveJavaType primitiveJavaType = (PrimitiveJavaType) selectionExpressibleJavaType; + if ( primitiveJavaType.getPrimitiveClass() == resultClass ) { + return; + } + } + + if ( isMatchingDateType( selectionExpressibleJavaTypeClass, resultClass, selectionExpressible ) ) { + return; + } + + if ( isEntityIdType( selectionExpressible, resultClass ) ) { + return; + } + + throwQueryTypeMismatchException( resultClass, selectionExpressible ); + } + } + + private static boolean isEntityIdType(SqmExpressible selectionExpressible, Class resultClass) { + if ( selectionExpressible instanceof IdentifiableDomainType ) { + final IdentifiableDomainType identifiableDomainType = (IdentifiableDomainType) selectionExpressible; + final SimpleDomainType idType = identifiableDomainType.getIdType(); + return resultClass.isAssignableFrom( idType.getBindableJavaType() ); + } + else if ( selectionExpressible instanceof EntitySqmPathSource ) { + final EntitySqmPathSource entityPath = (EntitySqmPathSource) selectionExpressible; + final EntityDomainType entityType = entityPath.getSqmPathType(); + final SimpleDomainType idType = entityType.getIdType(); + return resultClass.isAssignableFrom( idType.getBindableJavaType() ); + } + + return false; } // Special case for date because we always report java.util.Date as expression type @@ -785,7 +848,7 @@ public abstract class AbstractSelectionQuery super.collectHints( hints ); if ( isReadOnly() ) { - hints.put( HINT_READONLY, true ); + hints.put( HINT_READ_ONLY, true ); } putIfNotNull( hints, HINT_FETCH_SIZE, getFetchSize() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngine.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngine.java index 522dabd2d6..c902f04572 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngine.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngine.java @@ -49,5 +49,9 @@ public interface QueryEngine { HqlTranslator getHqlTranslator(); SqmTranslatorFactory getSqmTranslatorFactory(); + + default HqlInterpretation interpretHql(String hql, Class resultType) { + return getInterpretationCache().resolveHqlInterpretation( hql, resultType, getHqlTranslator() ); + } } 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 95591bc779..0f9ddf564c 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 @@ -107,11 +107,11 @@ public class SqmSelectionQueryImpl extends AbstractSqmSelectionQuery this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); this.expectedResultType = expectedResultType; -// visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); + visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); this.resultType = determineResultType( sqm ); + this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); setComment( hql ); - this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } private Class determineResultType(SqmSelectStatement sqm) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicCriteriaResultTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicCriteriaResultTests.java index 3a8ba44b5b..12180579fb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicCriteriaResultTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicCriteriaResultTests.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Steve Ebersole */ -@DomainModel( annotatedClasses = SimpleEntity.class ) +@DomainModel( annotatedClasses = {SimpleEntity.class, Dto.class, Dto2.class } ) @SessionFactory public class BasicCriteriaResultTests { @BeforeEach diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicHqlResultTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicHqlResultTests.java index a8feeea8ce..6bb15c797c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicHqlResultTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/BasicHqlResultTests.java @@ -21,7 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Steve Ebersole */ -@DomainModel( annotatedClasses = SimpleEntity.class ) +@DomainModel( annotatedClasses = {SimpleEntity.class, Dto.class, Dto2.class } ) @SessionFactory public class BasicHqlResultTests { @BeforeEach diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto.java new file mode 100644 index 0000000000..486d04ca2b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +import org.hibernate.annotations.Imported; + +/** + * @author Steve Ebersole + */ +@Imported +public class Dto { + private final Integer key; + private final String text; + + public Dto(Integer key, String text) { + this.key = key; + this.text = text; + } + + public Integer getKey() { + return key; + } + + public String getText() { + return text; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto2.java new file mode 100644 index 0000000000..2b9e35900d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Dto2.java @@ -0,0 +1,25 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +import org.hibernate.annotations.Imported; + +/** + * @author Steve Ebersole + */ +@Imported +public class Dto2 { + private final String text; + + public Dto2(String text) { + this.text = text; + } + + public String getText() { + return text; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitInstantiationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitInstantiationTests.java new file mode 100644 index 0000000000..9962d3dfc3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitInstantiationTests.java @@ -0,0 +1,140 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaRoot; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = {SimpleEntity.class, SimpleComposite.class, Dto.class, Dto2.class}) +@SessionFactory +@SuppressWarnings("JUnitMalformedDeclaration") +public class ImplicitInstantiationTests { + @BeforeEach + void prepareTestData(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + session.persist( new SimpleEntity( 1, "first", new SimpleComposite( "value1", "value2" ) ) ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + session.createMutationQuery( "delete SimpleEntity" ).executeUpdate(); + }); + } + + @Test + void testCreateQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Dto rtn = session.createQuery( Queries.ID_NAME, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createQuery( Queries.ID_COMP_VAL, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + } + + @Test + void testCreateSelectionQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Dto rtn = session.createSelectionQuery( Queries.ID_NAME, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createSelectionQuery( Queries.ID_COMP_VAL, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + } + + @Test + void testCriteria(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final HibernateCriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + + final JpaCriteriaQuery criteria = criteriaBuilder.createQuery( Dto.class ); + final JpaRoot root = criteria.from( SimpleEntity.class ); + criteria.multiselect( root.get( "id" ), root.get( "name" ) ); + final Dto rtn = session.createQuery( criteria ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18306" ) + void testCreateQuerySingleSelectItem(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Dto2 rtn = session.createQuery( Queries.NAME, Dto2.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto2 rtn = session.createQuery( Queries.COMP_VAL, Dto2.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18306" ) + void testCreateSelectionQuerySingleSelectItem(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final Dto2 rtn = session.createSelectionQuery( Queries.NAME, Dto2.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto2 rtn = session.createSelectionQuery( Queries.COMP_VAL, Dto2.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18306" ) + void testCriteriaSingleSelectItem(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final HibernateCriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + + final JpaCriteriaQuery criteria = criteriaBuilder.createQuery( Dto2.class ); + final JpaRoot root = criteria.from( SimpleEntity.class ); + criteria.multiselect( root.get( "name" ) ); + final Dto2 rtn = session.createQuery( criteria ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/InvalidReturnTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/InvalidReturnTests.java new file mode 100644 index 0000000000..6b9178ac50 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/InvalidReturnTests.java @@ -0,0 +1,147 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +import org.hibernate.query.QueryTypeMismatchException; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-18401" ) +@DomainModel(annotatedClasses = {SimpleEntity.class, SimpleComposite.class, Dto.class, Dto2.class}) +@SessionFactory +@SuppressWarnings("JUnitMalformedDeclaration") +public class InvalidReturnTests { + + @Test + void testCreateQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + try { + session.createQuery( Queries.ENTITY, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createQuery( Queries.ENTITY, Dto.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createQuery( Queries.ENTITY_NO_SELECT, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createQuery( Queries.COMPOSITE, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createQuery( Queries.ID_NAME_DTO, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + } ); + } + + @Test + void testCreateSelectionQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + try { + session.createSelectionQuery( Queries.ENTITY, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createSelectionQuery( Queries.ENTITY_NO_SELECT, Dto.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createSelectionQuery( Queries.ENTITY_NO_SELECT, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createSelectionQuery( Queries.COMPOSITE, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createSelectionQuery( Queries.ID_NAME_DTO, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + } ); + } + + @Test + void testNamedQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + try { + session.createNamedQuery( Queries.NAMED_ENTITY, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createNamedQuery( Queries.NAMED_ENTITY_NO_SELECT, Dto.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createNamedQuery( Queries.NAMED_ENTITY_NO_SELECT, SimpleComposite.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createNamedQuery( Queries.NAMED_COMPOSITE, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + + try { + session.createNamedQuery( Queries.NAMED_ID_NAME_DTO, SimpleEntity.class ); + fail( "Expecting a QueryTypeMismatchException" ); + } + catch (QueryTypeMismatchException expected) { + } + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Queries.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Queries.java new file mode 100644 index 0000000000..379aab1cc5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/Queries.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +/** + * @author Steve Ebersole + */ +public class Queries { + public static final String ENTITY = "select e from SimpleEntity e"; + public static final String ENTITY_NO_SELECT = "from SimpleEntity e"; + + public static final String COMPOSITE = "select e.composite from SimpleEntity e"; + public static final String NAME = "select e.name from SimpleEntity e"; + public static final String COMP_VAL = "select e.composite.value1 from SimpleEntity e"; + + public static final String ID_NAME = "select e.id, e.name from SimpleEntity e"; + public static final String ID_COMP_VAL = "select e.id, e.composite.value1 from SimpleEntity e"; + + public static final String ID_NAME_DTO = "select new Dto(e.id, e.name) from SimpleEntity e"; + public static final String ID_COMP_VAL_DTO = "select new Dto(e.id, e.composite.value1) from SimpleEntity e"; + + public static final String NAME_DTO = "select new Dto2(e.name) from SimpleEntity e"; + public static final String COMP_VAL_DTO = "select new Dto2(e.composite.value1) from SimpleEntity e"; + + public static final String NAMED_ENTITY = "entity"; + public static final String NAMED_ENTITY_NO_SELECT = "entity-no-select"; + public static final String NAMED_COMPOSITE = "composite"; + public static final String NAMED_NAME = "name"; + public static final String NAMED_COMP_VAL = "comp-val"; + public static final String NAMED_ID_NAME = "id-name"; + public static final String NAMED_ID_COMP_VAL = "id-comp-val"; + public static final String NAMED_ID_NAME_DTO = "id-name-dto"; + public static final String NAMED_ID_COMP_VAL_DTO = "id-comp-val-dto"; + public static final String NAMED_NAME_DTO = "name-dto"; + public static final String NAMED_COMP_VAL_DTO = "comp-val-dto"; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/SimpleEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/SimpleEntity.java index a8368586b2..43b835ae99 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/SimpleEntity.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/SimpleEntity.java @@ -7,8 +7,8 @@ package org.hibernate.orm.test.query.results; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; import jakarta.persistence.Table; /** @@ -16,6 +16,17 @@ import jakarta.persistence.Table; */ @Entity(name = "SimpleEntity") @Table(name = "simple_entity") +@NamedQuery(name= Queries.NAMED_ENTITY, query = Queries.ENTITY) +@NamedQuery(name= Queries.NAMED_ENTITY_NO_SELECT, query = Queries.ENTITY_NO_SELECT) +@NamedQuery(name= Queries.NAMED_COMPOSITE, query = Queries.COMPOSITE) +@NamedQuery(name= Queries.NAMED_NAME, query = Queries.NAME) +@NamedQuery(name= Queries.NAMED_COMP_VAL, query = Queries.COMP_VAL) +@NamedQuery(name= Queries.NAMED_ID_NAME, query = Queries.ID_NAME) +@NamedQuery(name= Queries.NAMED_ID_COMP_VAL, query = Queries.ID_COMP_VAL) +@NamedQuery(name= Queries.NAMED_ID_NAME_DTO, query = Queries.ID_NAME_DTO) +@NamedQuery(name= Queries.NAMED_ID_COMP_VAL_DTO, query = Queries.ID_COMP_VAL_DTO) +@NamedQuery(name= Queries.NAMED_NAME_DTO, query = Queries.NAME_DTO) +@NamedQuery(name= Queries.NAMED_COMP_VAL_DTO, query = Queries.COMP_VAL_DTO) public class SimpleEntity { @Id public Integer id; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/TypedQueryCreationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/TypedQueryCreationTests.java new file mode 100644 index 0000000000..c3f2327f37 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/TypedQueryCreationTests.java @@ -0,0 +1,202 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.results; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaRoot; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-18401" ) +@DomainModel(annotatedClasses = {SimpleEntity.class, SimpleComposite.class, Dto.class, Dto2.class}) +@SessionFactory +@SuppressWarnings("JUnitMalformedDeclaration") +public class TypedQueryCreationTests { + @BeforeEach + void prepareTestData(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + session.persist( new SimpleEntity( 1, "first", new SimpleComposite( "value1", "value2" ) ) ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + session.createMutationQuery( "delete SimpleEntity" ).executeUpdate(); + }); + } + + @Test + void testCreateQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createQuery( Queries.ENTITY, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createQuery( Queries.ENTITY_NO_SELECT, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleComposite rtn = session.createQuery( Queries.COMPOSITE, SimpleComposite.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.value1 ).isEqualTo( "value1" ); + assertThat( rtn.value2 ).isEqualTo( "value2" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createQuery( Queries.ID_NAME_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createQuery( Queries.ID_COMP_VAL_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createQuery( Queries.NAME, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createQuery( Queries.COMP_VAL, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "value1" ); + } ); + } + + @Test + void testCreateSelectionQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createSelectionQuery( Queries.ENTITY, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createSelectionQuery( Queries.ENTITY_NO_SELECT, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleComposite rtn = session.createSelectionQuery( Queries.COMPOSITE, SimpleComposite.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.value1 ).isEqualTo( "value1" ); + assertThat( rtn.value2 ).isEqualTo( "value2" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createSelectionQuery( Queries.ID_NAME_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createSelectionQuery( Queries.ID_COMP_VAL_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createSelectionQuery( Queries.NAME, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createSelectionQuery( Queries.COMP_VAL, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "value1" ); + } ); + } + + @Test + void testCreateNamedQuery(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createNamedQuery( Queries.NAMED_ENTITY, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleEntity rtn = session.createNamedQuery( Queries.NAMED_ENTITY_NO_SELECT, SimpleEntity.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + + sessions.inTransaction( (session) -> { + final SimpleComposite rtn = session.createNamedQuery( Queries.NAMED_COMPOSITE, SimpleComposite.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.value1 ).isEqualTo( "value1" ); + assertThat( rtn.value2 ).isEqualTo( "value2" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createNamedQuery( Queries.NAMED_ID_NAME_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final Dto rtn = session.createNamedQuery( Queries.NAMED_ID_COMP_VAL_DTO, Dto.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.getKey() ).isEqualTo( 1 ); + assertThat( rtn.getText() ).isEqualTo( "value1" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createNamedQuery( Queries.NAMED_NAME, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "first" ); + } ); + + sessions.inTransaction( (session) -> { + final String rtn = session.createNamedQuery( Queries.NAMED_COMP_VAL, String.class ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn ).isEqualTo( "value1" ); + } ); + } + + @Test + void testCriteria(SessionFactoryScope sessions) { + sessions.inTransaction( (session) -> { + final HibernateCriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + + final JpaCriteriaQuery criteria = criteriaBuilder.createQuery( SimpleEntity.class ); + final JpaRoot root = criteria.from( SimpleEntity.class ); + criteria.select( root ); + final SimpleEntity rtn = session.createQuery( criteria ).getSingleResultOrNull(); + assertThat( rtn ).isNotNull(); + assertThat( rtn.id ).isEqualTo( 1 ); + } ); + } +}