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 9a98a01aa60b..bab9f8818550 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -18,6 +18,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; @@ -905,22 +906,8 @@ public QueryImplementor createQuery(TypedQueryReference typedQueryRefe // 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") @@ -953,12 +940,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/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 4714942c2833..1a692df5b35e 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 @@ -17,8 +17,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.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.query.QueryFlushMode; import org.hibernate.HibernateException; @@ -105,10 +109,13 @@ import jakarta.persistence.TypedQuery; import jakarta.persistence.metamodel.SingularAttribute; import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import org.hibernate.type.spi.TypeConfiguration; import static java.lang.Character.isWhitespace; import static java.util.Collections.addAll; +import static org.hibernate.internal.util.ReflectHelper.isClass; import static org.hibernate.internal.util.StringHelper.unqualify; import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; @@ -116,6 +123,7 @@ import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; import static org.hibernate.query.results.internal.Builders.resultClassBuilder; import static org.hibernate.query.results.ResultSetMapping.resolveResultSetMapping; +import static org.hibernate.query.sqm.internal.SqmUtil.isResultTypeAlwaysAllowed; /** * @author Steve Ebersole @@ -129,6 +137,7 @@ public class NativeQueryImpl private final List parameterOccurrences; private final QueryParameterBindings parameterBindings; + private final Class resultType; private final ResultSetMapping resultSetMapping; private final boolean resultMappingSuppliedToCtor; @@ -166,6 +175,7 @@ public NativeQueryImpl( return false; } }, + null, session ); } @@ -218,26 +228,9 @@ public NativeQueryImpl( 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" ); - } - } } /** @@ -258,6 +251,7 @@ public NativeQueryImpl( mappingMemento.resolve( resultSetMapping, querySpaceConsumer, context ); return true; }, + null, session ); @@ -268,6 +262,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(); @@ -279,6 +282,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(); @@ -286,6 +290,27 @@ public NativeQueryImpl( this.resultMappingSuppliedToCtor = resultSetMappingHandler.resolveResultSetMapping( resultSetMapping, querySpaces::add, this ); + 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 ); } @@ -301,6 +326,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 ); @@ -310,6 +336,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<>(); @@ -320,11 +350,46 @@ 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 = 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 = getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ); + return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class; + } + @FunctionalInterface private interface ResultSetMappingHandler { boolean resolveResultSetMapping( @@ -436,11 +501,16 @@ public QueryParameterBindings getParameterBindings() { return getQueryParameterBindings(); } + @Override + public Class getResultType() { + return resultType; + } + @Override public NamedNativeQueryMemento toMemento(String name) { return new NamedNativeQueryMementoImpl<>( name, - extractResultClass( resultSetMapping ), + resultType != null ? resultType : extractResultClass( resultSetMapping ), sqlString, originalSqlString, resultSetMapping.getMappingIdentifier(), @@ -459,14 +529,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; @@ -618,13 +688,29 @@ public KeyedResultList getKeyedResultList(KeyedPage page) { } protected SelectQueryPlan resolveSelectQueryPlan() { + 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 { + mapping = resultSetMapping; + } return isCacheableQuery() - ? getInterpretationCache() - .resolveSelectQueryPlan( selectInterpretationsKey(), this::createQueryPlan ) - : createQueryPlan(); + ? getInterpretationCache().resolveSelectQueryPlan( selectInterpretationsKey( mapping ), () -> createQueryPlan( mapping ) ) + : createQueryPlan( mapping ); } - private NativeSelectQueryPlan createQueryPlan() { + private NativeSelectQueryPlan createQueryPlan(ResultSetMapping resultSetMapping) { final NativeSelectQueryDefinition queryDefinition = new NativeSelectQueryDefinition<>() { final String sqlString = expandParameterLists(); @@ -834,7 +920,7 @@ public static int determineBindValueMaxCount(boolean paddingEnabled, int inExprL return bindValueMaxCount; } - private SelectInterpretationsKey selectInterpretationsKey() { + private SelectInterpretationsKey selectInterpretationsKey(ResultSetMapping resultSetMapping) { return new SelectInterpretationsKey( getQueryString(), resultSetMapping, 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 98b2fd488e59..6d2cb3c84978 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 @@ -18,6 +18,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; @@ -26,6 +27,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; @@ -277,6 +279,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() @@ -315,13 +356,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() ); } ); } @@ -329,7 +373,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(); + } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/EntityReturnClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/EntityReturnClassTests.java index 88f8c47a6141..5563f2d944a6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/EntityReturnClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/EntityReturnClassTests.java @@ -10,7 +10,6 @@ import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.JiraKey; -import org.hibernate.testing.orm.junit.NotImplementedYet; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -63,7 +62,6 @@ public void testAddRootNoReturn(SessionFactoryScope scope) { @Test @JiraKey("HHH-18864") - @NotImplementedYet public void testAddEntityWithEntityReturn(SessionFactoryScope scope) { scope.inTransaction( (session) -> { NativeQuery query = session.createNativeQuery( "select {s.*} from Speech s", Speech.class ); @@ -75,7 +73,6 @@ public void testAddEntityWithEntityReturn(SessionFactoryScope scope) { @Test @JiraKey("HHH-18864") - @NotImplementedYet public void testAddRootWithEntityReturn(SessionFactoryScope scope) { scope.inTransaction( (session) -> { NativeQuery query = session.createNativeQuery( "select {s.*} from Speech s", Speech.class ); @@ -91,7 +88,6 @@ public void testAddRootWithEntityReturn(SessionFactoryScope scope) { @Test @JiraKey("HHH-18864") - @NotImplementedYet public void testAddEntityWithInterfaceReturn(SessionFactoryScope scope) { scope.inTransaction( (session) -> { NativeQuery query = session.createNativeQuery( "select {s.*} from Speech s", SpeechInterface.class ); @@ -103,7 +99,6 @@ public void testAddEntityWithInterfaceReturn(SessionFactoryScope scope) { @Test @JiraKey("HHH-18864") - @NotImplementedYet public void testAddRootWithInterfaceReturn(SessionFactoryScope scope) { scope.inTransaction( (session) -> { NativeQuery query = session.createNativeQuery( "select {s.*} from Speech s", SpeechInterface.class );