From 3b79d2702f00e13f2173952bce10d846b8f252fb Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 21 Nov 2024 16:44:28 +0100 Subject: [PATCH] HHH-18629 Fix inconsistent column alias generated while result class is used for placeholder --- .../internal/NamedHqlQueryDefinitionImpl.java | 1 + .../NamedNativeQueryDefinitionImpl.java | 11 +- .../AbstractSharedSessionContract.java | 43 ++--- .../NamedCallableQueryMementoImpl.java | 1 + .../NamedCriteriaQueryMementoImpl.java | 6 +- .../internal/NamedHqlQueryMementoImpl.java | 5 + .../named/AbstractNamedQueryMemento.java | 9 ++ .../internal/NamedNativeQueryMementoImpl.java | 6 +- .../query/sql/internal/NativeQueryImpl.java | 152 +++++++++++++----- .../sql/spi/NamedNativeQueryMemento.java | 4 + .../query/sqm/internal/QuerySqmImpl.java | 2 + .../sql/NativeQueryResultBuilderTests.java | 53 +++++- 12 files changed, 227 insertions(+), 66 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java index 284a97cf9699..5bdb30e81522 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java @@ -70,6 +70,7 @@ public String getHqlString() { public NamedSqmQueryMemento resolve(SessionFactoryImplementor factory) { return new NamedHqlQueryMementoImpl( getRegistrationName(), + null, hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java index 688d8dc5e16e..987b873730b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java @@ -18,6 +18,8 @@ import org.hibernate.query.sql.internal.NamedNativeQueryMementoImpl; import org.hibernate.query.sql.spi.NamedNativeQueryMemento; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.StringHelper.isNotEmpty; /** @@ -86,15 +88,16 @@ public String getResultSetMappingClassName() { @Override public NamedNativeQueryMemento resolve(SessionFactoryImplementor factory) { + Class resultClass = isNotEmpty( resultSetMappingClassName ) + ? factory.getServiceRegistry().requireService( ClassLoaderService.class ).classForName( resultSetMappingClassName ) + : null; return new NamedNativeQueryMementoImpl( getRegistrationName(), + resultClass, sqlString, sqlString, resultSetMappingName, - isNotEmpty( resultSetMappingClassName ) - ? factory.getServiceRegistry().requireService( ClassLoaderService.class ) - .classForName( resultSetMappingClassName ) - : null, + resultClass, querySpaces, getCacheable(), getCacheRegion(), 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 3a3e80c088dc..e24c4a54d7c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -19,6 +19,7 @@ import java.util.function.Function; import jakarta.persistence.EntityGraph; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.EntityNameResolver; import org.hibernate.Filter; @@ -891,22 +892,8 @@ public QueryImplementor createQuery(String queryString, Class expected // dynamic native (SQL) query handling @Override @SuppressWarnings("rawtypes") - public NativeQueryImpl createNativeQuery(String sqlString) { - checkOpen(); - pulseTransactionCoordinator(); - delayedAfterCompletion(); - - try { - final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, this ); - if ( isEmpty( query.getComment() ) ) { - query.setComment( "dynamic native SQL query" ); - } - applyQuerySettingsAndHints( query ); - return query; - } - catch (RuntimeException he) { - throw getExceptionConverter().convert( he ); - } + public NativeQueryImplementor createNativeQuery(String sqlString) { + return createNativeQuery( sqlString, (Class) null ); } @Override @SuppressWarnings("rawtypes") @@ -939,12 +926,28 @@ protected NamedResultSetMappingMemento getResultSetMappingMemento(String resultS @Override @SuppressWarnings({"rawtypes", "unchecked"}) //note: we're doing something a bit funny here to work around // the clashing signatures declared by the supertypes - public NativeQueryImplementor createNativeQuery(String sqlString, Class resultClass) { - final NativeQueryImpl query = createNativeQuery( sqlString ); - addResultType( resultClass, query ); - return query; + public NativeQueryImplementor createNativeQuery(String sqlString, @Nullable Class resultClass) { + checkOpen(); + pulseTransactionCoordinator(); + delayedAfterCompletion(); + + try { + final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, resultClass, this ); + if ( isEmpty( query.getComment() ) ) { + query.setComment( "dynamic native SQL query" ); + } + applyQuerySettingsAndHints( query ); + return query; + } + catch (RuntimeException he) { + throw getExceptionConverter().convert( he ); + } } + /** + * @deprecated Use {@link NativeQueryImpl#NativeQueryImpl(String, Class, SharedSessionContractImplementor)} instead + */ + @Deprecated(forRemoval = true) protected void addResultType(Class resultClass, NativeQueryImplementor query) { if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java index d79f9d4aba49..e7ffc89325dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java @@ -64,6 +64,7 @@ public NamedCallableQueryMementoImpl( Map hints) { super( name, + Object.class, cacheable, cacheRegion, cacheMode, diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java index 1c0ef4f2f623..049c4dab3a1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento implements NamedSqmQueryMemento, Serializable { private final SqmStatement sqmStatement; @@ -33,6 +35,7 @@ public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento imp public NamedCriteriaQueryMementoImpl( String name, + @Nullable Class resultType, SqmStatement sqmStatement, Integer firstResult, Integer maxResults, @@ -47,7 +50,7 @@ public NamedCriteriaQueryMementoImpl( String comment, Map parameterTypes, Map hints) { - super( name, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); + super( name, resultType, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); this.sqmStatement = sqmStatement; this.firstResult = firstResult; this.maxResults = maxResults; @@ -114,6 +117,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java index 2cc32c0eb3ff..272c4a7cd6bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Definition of a named query, defined in the mapping metadata. * Additionally, as of JPA 2.1, named query definition can also come @@ -41,6 +43,7 @@ public class NamedHqlQueryMementoImpl extends AbstractNamedQueryMemento implemen public NamedHqlQueryMementoImpl( String name, + @Nullable Class resultType, String hqlString, Integer firstResult, Integer maxResults, @@ -57,6 +60,7 @@ public NamedHqlQueryMementoImpl( Map hints) { super( name, + resultType, cacheable, cacheRegion, cacheMode, @@ -103,6 +107,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java index 960cdeb2d68f..14e1bc408fa2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java @@ -14,12 +14,15 @@ import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole * @author Gavin King */ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { private final String name; + private final @Nullable Class resultType; private final Boolean cacheable; private final String cacheRegion; @@ -37,6 +40,7 @@ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { protected AbstractNamedQueryMemento( String name, + @Nullable Class resultType, Boolean cacheable, String cacheRegion, CacheMode cacheMode, @@ -47,6 +51,7 @@ protected AbstractNamedQueryMemento( String comment, Map hints) { this.name = name; + this.resultType = resultType; this.cacheable = cacheable; this.cacheRegion = cacheRegion; this.cacheMode = cacheMode; @@ -63,6 +68,10 @@ public String getRegistrationName() { return name; } + public @Nullable Class getResultType() { + return resultType; + } + @Override public Boolean getCacheable() { return cacheable; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java index 8ba29214b482..5d9f383aae08 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java @@ -38,6 +38,7 @@ public class NamedNativeQueryMementoImpl extends AbstractNamedQueryMemento imple public NamedNativeQueryMementoImpl( String name, + Class resultClass, String sqlString, String originalSqlString, String resultSetMappingName, @@ -56,6 +57,7 @@ public NamedNativeQueryMementoImpl( Map hints) { super( name, + resultClass, cacheable, cacheRegion, cacheMode, @@ -123,6 +125,7 @@ public Integer getMaxResults() { public NamedNativeQueryMemento makeCopy(String name) { return new NamedNativeQueryMementoImpl( name, + getResultType(), sqlString, originalSqlString, resultSetMappingName, @@ -149,7 +152,8 @@ public void validate(QueryEngine queryEngine) { @Override public NativeQueryImplementor toQuery(SharedSessionContractImplementor session) { - return new NativeQueryImpl<>( this, session ); + //noinspection unchecked + return new NativeQueryImpl<>( this, (Class) getResultType(), session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 6179773fc609..c445e4a421f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -20,8 +20,12 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.hibernate.jpa.spi.NativeQueryConstructorTransformer; +import org.hibernate.jpa.spi.NativeQueryListTransformer; +import org.hibernate.jpa.spi.NativeQueryMapTransformer; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -55,7 +59,6 @@ import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; import org.hibernate.query.internal.QueryOptionsImpl; -import org.hibernate.query.internal.QueryParameterBindingsImpl; import org.hibernate.query.internal.ResultSetMappingResolutionContext; import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.Builders; @@ -92,7 +95,6 @@ import org.hibernate.query.sql.spi.SelectInterpretationsKey; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.BasicType; @@ -110,8 +112,13 @@ import jakarta.persistence.TemporalType; import jakarta.persistence.Tuple; import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; +import static org.hibernate.internal.util.ReflectHelper.isClass; +import static org.hibernate.internal.util.StringHelper.unqualify; import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; +import static org.hibernate.query.sqm.internal.SqmUtil.isResultTypeAlwaysAllowed; import static org.hibernate.query.results.Builders.resultClassBuilder; /** @@ -126,6 +133,7 @@ public class NativeQueryImpl private final List parameterOccurrences; private final QueryParameterBindings parameterBindings; + private final Class resultType; private final ResultSetMapping resultSetMapping; private final boolean resultMappingSuppliedToCtor; @@ -177,6 +185,7 @@ else if ( memento.getResultMappingClass() != null ) { return false; }, + null, session ); } @@ -206,39 +215,16 @@ public NativeQueryImpl( } } - if ( memento.getResultMappingClass() != null ) { - resultSetMapping.addResultBuilder( resultClassBuilder( - memento.getResultMappingClass(), - context - ) ); + if ( memento.getResultType() != null ) { + resultSetMapping.addResultBuilder( resultClassBuilder( memento.getResultType(), context ) ); return true; } return false; }, + resultJavaType, session ); - - if ( resultJavaType == Tuple.class ) { - setTupleTransformer( new NativeQueryTupleTransformer() ); - } - else if ( resultJavaType != null && !resultJavaType.isArray() ) { - switch ( resultSetMapping.getNumberOfResultBuilders() ) { - case 0: { - throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); - } - case 1: { - final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ).getJavaType(); - if ( actualResultJavaType != null && !resultJavaType.isAssignableFrom( actualResultJavaType ) ) { - throw buildIncompatibleException( resultJavaType, actualResultJavaType ); - } - break; - } - default: { - throw new IllegalArgumentException( "Cannot create TypedQuery for query with more than one return" ); - } - } - } } /** @@ -260,6 +246,7 @@ public NativeQueryImpl( mappingMemento.resolve( resultSetMapping, querySpaceConsumer, context ); return true; }, + null, session ); @@ -270,6 +257,15 @@ public NativeQueryImpl( Supplier resultSetMappingCreator, ResultSetMappingHandler resultSetMappingHandler, SharedSessionContractImplementor session) { + this( memento, resultSetMappingCreator, resultSetMappingHandler, null, session ); + } + + public NativeQueryImpl( + NamedNativeQueryMemento memento, + Supplier resultSetMappingCreator, + ResultSetMappingHandler resultSetMappingHandler, + @Nullable Class resultType, + SharedSessionContractImplementor session) { super( session ); this.originalSqlString = memento.getOriginalSqlString(); @@ -283,6 +279,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; this.querySpaces = new HashSet<>(); this.resultSetMapping = resultSetMappingCreator.get(); @@ -296,6 +293,27 @@ public NativeQueryImpl( this.resultMappingSuppliedToCtor = appliedAnyResults; + if ( resultType != null ) { + if ( !isResultTypeAlwaysAllowed( resultType ) ) { + switch ( resultSetMapping.getNumberOfResultBuilders() ) { + case 0: + throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); + case 1: + final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ) + .getJavaType(); + if ( actualResultJavaType != null && !resultType.isAssignableFrom( actualResultJavaType ) ) { + throw buildIncompatibleException( resultType, actualResultJavaType ); + } + break; + default: + throw new IllegalArgumentException( + "Cannot create TypedQuery for query with more than one return" ); + } + } + else { + setTupleTransformerForResultType( resultType ); + } + } applyOptions( memento ); } @@ -312,6 +330,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = null; this.querySpaces = new HashSet<>(); this.resultSetMapping = buildResultSetMapping( resultSetMappingMemento.getName(), false, session ); @@ -325,6 +344,10 @@ public NativeQueryImpl( } public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) { + this( sqlString, null, session ); + } + + public NativeQueryImpl(String sqlString, @Nullable Class resultType, SharedSessionContractImplementor session) { super( session ); this.querySpaces = new HashSet<>(); @@ -335,11 +358,47 @@ public NativeQueryImpl(String sqlString, SharedSessionContractImplementor sessio this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; + if ( resultType != null ) { + setTupleTransformerForResultType( resultType ); + } this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() ); this.resultMappingSuppliedToCtor = false; } + protected void setTupleTransformerForResultType(Class resultClass) { + final TupleTransformer tupleTransformer = determineTupleTransformerForResultType( resultClass ); + if ( tupleTransformer != null ) { + setTupleTransformer( tupleTransformer ); + } + } + + protected @Nullable TupleTransformer determineTupleTransformerForResultType(Class resultClass) { + if ( Tuple.class.equals( resultClass ) ) { + return NativeQueryTupleTransformer.INSTANCE; + } + else if ( Map.class.equals( resultClass ) ) { + return NativeQueryMapTransformer.INSTANCE; + } + else if ( List.class.equals( resultClass ) ) { + return NativeQueryListTransformer.INSTANCE; + } + else if ( resultClass != Object.class && resultClass != Object[].class ) { + if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { + // not a basic type + return new NativeQueryConstructorTransformer<>( resultClass ); + } + } + return null; + } + + private boolean hasJavaTypeDescriptor(Class resultClass) { + final JavaType descriptor = getSessionFactory().getTypeConfiguration().getJavaTypeRegistry() + .findDescriptor( resultClass ); + return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class; + } + @FunctionalInterface private interface ResultSetMappingHandler { boolean resolveResultSetMapping( @@ -456,10 +515,16 @@ public QueryParameterBindings getParameterBindings() { return getQueryParameterBindings(); } + @Override + public Class getResultType() { + return resultType; + } + @Override public NamedNativeQueryMemento toMemento(String name) { return new NamedNativeQueryMementoImpl( name, + resultType != null ? resultType : extractResultClass( resultSetMapping ), sqlString, originalSqlString, resultSetMapping.getMappingIdentifier(), @@ -479,14 +544,14 @@ public NamedNativeQueryMemento toMemento(String name) { ); } - private Class extractResultClass(ResultSetMapping resultSetMapping) { + private Class extractResultClass(ResultSetMapping resultSetMapping) { final List resultBuilders = resultSetMapping.getResultBuilders(); if ( resultBuilders.size() == 1 ) { final ResultBuilder resultBuilder = resultBuilders.get( 0 ); if ( resultBuilder instanceof ImplicitResultClassBuilder || resultBuilder instanceof ImplicitModelPartResultBuilderEntity || resultBuilder instanceof DynamicResultBuilderEntityCalculated ) { - return resultBuilder.getJavaType(); + return (Class) resultBuilder.getJavaType(); } } return null; @@ -645,19 +710,32 @@ public KeyedResultList getKeyedResultList(KeyedPage page) { } protected SelectQueryPlan resolveSelectQueryPlan() { - if ( isCacheableQuery() ) { - final QueryInterpretationCache.Key cacheKey = generateSelectInterpretationsKey( resultSetMapping ); - return getSession().getFactory().getQueryEngine().getInterpretationCache() - .resolveSelectQueryPlan( cacheKey, () -> createQueryPlan( resultSetMapping ) ); + final ResultSetMapping mapping; + if ( resultType != null && resultSetMapping.isDynamic() && resultSetMapping.getNumberOfResultBuilders() == 0 ) { + mapping = ResultSetMapping.resolveResultSetMapping( originalSqlString, true, getSessionFactory() ); + + if ( getSessionFactory().getMappingMetamodel().isEntityClass( resultType ) ) { + mapping.addResultBuilder( + Builders.entityCalculated( unqualify( resultType.getName() ), resultType.getName(), + LockMode.READ, getSessionFactory() ) ); + } + else if ( !isResultTypeAlwaysAllowed( resultType ) + && (!isClass( resultType ) || hasJavaTypeDescriptor( resultType )) ) { + mapping.addResultBuilder( Builders.resultClassBuilder( resultType, getSessionFactory() ) ); + } } else { - return createQueryPlan( resultSetMapping ); + mapping = resultSetMapping; } + return isCacheableQuery() + ? getSession().getFactory().getQueryEngine().getInterpretationCache().resolveSelectQueryPlan( selectInterpretationsKey( mapping ), () -> createQueryPlan( mapping ) ) + : createQueryPlan( mapping ); } private NativeSelectQueryPlan createQueryPlan(ResultSetMapping resultSetMapping) { - final String sqlString = expandParameterLists(); final NativeSelectQueryDefinition queryDefinition = new NativeSelectQueryDefinition<>() { + final String sqlString = expandParameterLists(); + @Override public String getSqlString() { return sqlString; @@ -864,7 +942,7 @@ public static int determineBindValueMaxCount(boolean paddingEnabled, int inExprL return bindValueMaxCount; } - private SelectInterpretationsKey generateSelectInterpretationsKey(JdbcValuesMappingProducer resultSetMapping) { + private SelectInterpretationsKey selectInterpretationsKey(ResultSetMapping resultSetMapping) { return new SelectInterpretationsKey( getQueryString(), resultSetMapping, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java index 54aad16b4cb4..8d4659b15f2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java @@ -9,6 +9,7 @@ import java.util.Set; import jakarta.persistence.SqlResultSetMapping; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.query.NamedNativeQueryDefinition; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -50,6 +51,8 @@ default String getOriginalSqlString(){ */ Class getResultMappingClass(); + @Nullable Class getResultType(); + Integer getFirstResult(); Integer getMaxResults(); @@ -135,6 +138,7 @@ public void setResultSetMappingClassName(String resultSetMappingClassName) { public NamedNativeQueryMemento build(SessionFactoryImplementor sessionFactory) { return new NamedNativeQueryMementoImpl( name, + null, queryString, queryString, resultSetMappingName, 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 785f0ccaf0ee..91fa946753ee 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 @@ -904,6 +904,7 @@ public NamedQueryMemento toMemento(String name) { } return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), @@ -923,6 +924,7 @@ public NamedQueryMemento toMemento(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), getQueryString(), getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java index b04e132ed4ea..147af62497c7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java @@ -20,6 +20,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter; import org.hibernate.query.sql.spi.NativeQueryImplementor; @@ -28,6 +29,7 @@ import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -279,6 +281,45 @@ public void testConvertedAttributeBasedBuilder(SessionFactoryScope scope) { ); } + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClass(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select data, id from BasicEntity"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClassAndPlaceholders(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select {be.*} from BasicEntity be"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + query.addEntity( "be", BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + @BeforeAll public void verifyModel(SessionFactoryScope scope) { final EntityMappingType entityDescriptor = scope.getSessionFactory() @@ -317,13 +358,16 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException entityOfBasics.setTheInstant( Instant.EPOCH ); session.persist( entityOfBasics ); + + session.persist( new BasicEntity( 1, STRING_VALUE ) ); } ); scope.inTransaction( session -> { - final EntityOfBasics entity = session.get( EntityOfBasics.class, 1 ); - assertThat( entity, notNullValue() ); + assertThat( session.get( EntityOfBasics.class, 1 ), notNullValue() ); + + assertThat( session.get( BasicEntity.class, 1 ), notNullValue() ); } ); } @@ -331,7 +375,10 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException @AfterEach public void cleanUpData(SessionFactoryScope scope) { scope.inTransaction( - session -> session.createQuery( "delete EntityOfBasics" ).executeUpdate() + session -> { + session.createQuery( "delete EntityOfBasics" ).executeUpdate(); + session.createQuery( "delete BasicEntity" ).executeUpdate(); + } ); }