From 263768d5c583152b8ea250ec4bf987149130f52c Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 22 Dec 2022 15:48:16 -0600 Subject: [PATCH] HHH-15930 - Support scalar resultClass in @NamedNativeQuery --- .../org/hibernate/userguide/model/Person.java | 4 +- .../org/hibernate/userguide/sql/SQLTest.java | 23 +++--- .../org/hibernate/query/results/Builders.java | 19 +++-- .../implicit/ImplicitResultClassBuilder.java | 70 +++++++++++++++++++ .../query/sql/internal/NativeQueryImpl.java | 47 ++++--------- .../JdbcSelectExecutorStandardImpl.java | 9 +-- .../jdbc/internal/ResultSetAccess.java | 10 +-- .../results/jdbc/spi/JdbcValuesMetadata.java | 16 ++++- 8 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java diff --git a/documentation/src/main/java/org/hibernate/userguide/model/Person.java b/documentation/src/main/java/org/hibernate/userguide/model/Person.java index a6848d51be..e6dff50b62 100644 --- a/documentation/src/main/java/org/hibernate/userguide/model/Person.java +++ b/documentation/src/main/java/org/hibernate/userguide/model/Person.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; + import jakarta.persistence.CascadeType; import jakarta.persistence.ColumnResult; import jakarta.persistence.ConstructorResult; @@ -41,7 +42,8 @@ import jakarta.persistence.Version; name = "find_person_name", query = "SELECT name " + - "FROM Person " + "FROM Person ", + resultClass = String.class ) //end::sql-scalar-NamedNativeQuery-example[] //tag::sql-multiple-scalar-values-NamedNativeQuery-example[] diff --git a/documentation/src/test/java/org/hibernate/userguide/sql/SQLTest.java b/documentation/src/test/java/org/hibernate/userguide/sql/SQLTest.java index 7cbeb19483..8383d928a8 100644 --- a/documentation/src/test/java/org/hibernate/userguide/sql/SQLTest.java +++ b/documentation/src/test/java/org/hibernate/userguide/sql/SQLTest.java @@ -9,14 +9,13 @@ package org.hibernate.userguide.sql; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; -import jakarta.persistence.PersistenceException; import org.hibernate.Session; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.loader.NonUniqueDiscoveredSqlAliasException; +import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.proxy.HibernateProxy; import org.hibernate.query.TupleTransformer; import org.hibernate.transform.Transformers; @@ -38,6 +37,9 @@ import org.hibernate.testing.TestForIssue; import org.junit.Before; import org.junit.Test; +import jakarta.persistence.PersistenceException; + +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; import static org.junit.Assert.assertEquals; @@ -586,7 +588,7 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-scalar-named-query-example[] List names = entityManager.createNamedQuery( - "find_person_name") + "find_person_name", String.class) .getResultList(); //end::sql-jpa-scalar-named-query-example[] assertEquals(3, names.size()); @@ -611,7 +613,7 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-multiple-scalar-values-named-query-example[] List tuples = entityManager.createNamedQuery( - "find_person_name_and_nickName") + "find_person_name_and_nickName", Object[].class) .getResultList(); for(Object[] tuple : tuples) { @@ -646,10 +648,12 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-multiple-scalar-values-dto-named-query-example[] List personNames = entityManager.createNamedQuery( - "find_person_name_and_nickName_dto") + "find_person_name_and_nickName_dto", PersonNames.class) .getResultList(); //end::sql-jpa-multiple-scalar-values-dto-named-query-example[] assertEquals(3, personNames.size()); + assertThat( personNames.get(0) ).isNotNull(); + assertThat( personNames.get(0) ).isInstanceOf(PersonNames.class); }); } @@ -686,11 +690,13 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-entity-named-query-example[] List persons = entityManager.createNamedQuery( - "find_person_by_name") + "find_person_by_name", Person.class) .setParameter("name", "J%") .getResultList(); //end::sql-jpa-entity-named-query-example[] assertEquals(1, persons.size()); + assertThat( persons ).hasSize( 1 ); + assertThat( persons.get( 0 ) ).isInstanceOf( Person.class ); }); } @@ -716,7 +722,7 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-entity-associations_named-query-example[] List tuples = entityManager.createNamedQuery( - "find_person_with_phones_by_name") + "find_person_with_phones_by_name", Object[].class) .setParameter("name", "J%") .getResultList(); @@ -726,6 +732,7 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { } //end::sql-jpa-entity-associations_named-query-example[] assertEquals(1, tuples.size()); + assertThat( tuples.get(0).getClass().isArray() ).isTrue(); }); } @@ -756,7 +763,7 @@ public class SQLTest extends BaseEntityManagerFunctionalTestCase { doInJPA(this::entityManagerFactory, entityManager -> { //tag::sql-jpa-composite-key-entity-associations_named-query-example[] List tuples = entityManager.createNamedQuery( - "find_all_spaceships") + "find_all_spaceships", Object[].class) .getResultList(); for(Object[] tuple : tuples) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java b/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java index f1ead40fc8..899cec4663 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java @@ -21,6 +21,7 @@ import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.DiscriminatedAssociationAttributeMapping; import org.hibernate.metamodel.mapping.internal.EntityCollectionPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.NativeQuery; import org.hibernate.query.internal.ResultSetMappingResolutionContext; @@ -40,6 +41,7 @@ import org.hibernate.query.results.implicit.ImplicitFetchBuilderEntity; import org.hibernate.query.results.implicit.ImplicitFetchBuilderEntityPart; import org.hibernate.query.results.implicit.ImplicitFetchBuilderPlural; import org.hibernate.query.results.implicit.ImplicitModelPartResultBuilderEntity; +import org.hibernate.query.results.implicit.ImplicitResultClassBuilder; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetchable; @@ -241,14 +243,23 @@ public class Builders { return new DynamicFetchBuilderLegacy( tableAlias, ownerTableAlias, joinPropertyName, new ArrayList<>(), new HashMap<>() ); } - public static ResultBuilder implicitEntityResultBuilder( + public static ResultBuilder resultClassBuilder( Class resultMappingClass, ResultSetMappingResolutionContext resolutionContext) { - final EntityMappingType entityMappingType = resolutionContext + final MappingMetamodelImplementor mappingMetamodel = resolutionContext .getSessionFactory() .getRuntimeMetamodels() - .getEntityMappingType( resultMappingClass ); - return new ImplicitModelPartResultBuilderEntity( entityMappingType ); + .getMappingMetamodel(); + final EntityMappingType entityMappingType = mappingMetamodel.findEntityDescriptor( resultMappingClass ); + if ( entityMappingType != null ) { + // the resultClass is an entity + return new ImplicitModelPartResultBuilderEntity( entityMappingType ); + } + + // todo : support for known embeddables might be nice + + // otherwise, assume it's a "basic" mapping + return new ImplicitResultClassBuilder( resultMappingClass ); } public static ImplicitFetchBuilder implicitFetchBuilder( diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java new file mode 100644 index 0000000000..e9fe15cce5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java @@ -0,0 +1,70 @@ +/* + * 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.query.results.implicit; + +import java.util.function.BiFunction; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.query.results.ResultBuilder; +import org.hibernate.query.results.dynamic.DynamicFetchBuilderLegacy; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.NamedNativeQuery; + +/** + * ResultBuilder for handling {@link NamedNativeQuery#resultClass()} when the + * class does not refer to an entity + * + * @author Steve Ebersole + */ +public class ImplicitResultClassBuilder implements ResultBuilder { + private final Class suppliedResultClass; + + public ImplicitResultClassBuilder(Class suppliedResultClass) { + this.suppliedResultClass = suppliedResultClass; + } + + @Override + public DomainResult buildResult( + JdbcValuesMetadata jdbcResultsMetadata, + int resultPosition, + BiFunction legacyFetchResolver, + DomainResultCreationState domainResultCreationState) { + final MappingMetamodelImplementor mappingMetamodel = domainResultCreationState.getSqlAstCreationState() + .getCreationContext() + .getMappingMetamodel(); + final TypeConfiguration typeConfiguration = mappingMetamodel.getTypeConfiguration(); + final int jdbcResultPosition = resultPosition + 1; + + final BasicType basicType = jdbcResultsMetadata.resolveType( + jdbcResultPosition, + typeConfiguration.getJavaTypeRegistry().resolveDescriptor( suppliedResultClass ), + typeConfiguration + ); + + return new BasicResult<>( + resultPosition, + jdbcResultsMetadata.resolveColumnName( jdbcResultPosition ), + basicType + ); + } + + @Override + public Class getJavaType() { + return suppliedResultClass; + } + + @Override + public ResultBuilder cacheKeyInstance() { + return this; + } +} 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 78eeda2db9..0f77343091 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,6 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; import org.hibernate.CacheMode; import org.hibernate.FlushMode; import org.hibernate.HibernateException; @@ -31,7 +29,6 @@ import org.hibernate.MappingException; import org.hibernate.QueryException; import org.hibernate.ScrollMode; import org.hibernate.dialect.Dialect; -import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.query.spi.NativeQueryInterpreter; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -92,6 +89,8 @@ import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeReference; import jakarta.persistence.AttributeConverter; +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.FlushModeType; @@ -103,6 +102,7 @@ import jakarta.persistence.Tuple; import jakarta.persistence.metamodel.SingularAttribute; import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; +import static org.hibernate.query.results.Builders.resultClassBuilder; /** * @author Steve Ebersole @@ -157,7 +157,7 @@ public class NativeQueryImpl if ( memento.getResultMappingClass() != null ) { resultSetMapping.addResultBuilder( - Builders.implicitEntityResultBuilder( + resultClassBuilder( memento.getResultMappingClass(), context ) @@ -239,36 +239,18 @@ public class NativeQueryImpl } if ( memento.getResultMappingClass() != null ) { - resultSetMapping.addResultBuilder( - Builders.implicitEntityResultBuilder( - memento.getResultMappingClass(), - context - ) - ); + resultSetMapping.addResultBuilder( resultClassBuilder( + memento.getResultMappingClass(), + context + ) ); return true; } -// if ( resultJavaType != null && resultJavaType != Tuple.class ) { - // todo (6.0): in 5.x we didn't add implicit result builders and by doing so, - // the result type check at the end of the constructor will fail like in 5.x -// final JpaMetamodel jpaMetamodel = context.getSessionFactory().getJpaMetamodel(); -// if ( jpaMetamodel.findEntityType( resultJavaType ) != null ) { -// resultSetMapping.addResultBuilder( -// Builders.implicitEntityResultBuilder( resultJavaType, context ) -// ); -// } -// else { -// resultSetMapping.addResultBuilder( -// Builders.scalar( -// 1, -// context.getSessionFactory() -// .getTypeConfiguration() -// .getBasicTypeForJavaType( resultJavaType ) -// ) -// ); -// } -// + + if ( resultJavaType != null && resultJavaType != Tuple.class && !resultJavaType.isArray() ) { + // todo : allow the expected Java type imply a builder to use +// resultSetMapping.addResultBuilder( resultClassBuilder( resultJavaType, context ) ); // return true; -// } + } return false; }, @@ -281,8 +263,9 @@ public class NativeQueryImpl else if ( resultJavaType != null && resultJavaType != Object[].class ) { switch ( resultSetMapping.getNumberOfResultBuilders() ) { case 0: - throw new IllegalArgumentException( "Named query exists but its result type is not compatible" ); + throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); case 1: + // would be nice to support types that are "wrappable", as in `JavaType#wrap` final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ).getJavaType(); if ( actualResultJavaType != null && !resultJavaType.isAssignableFrom( actualResultJavaType ) ) { throw buildIncompatibleException( resultJavaType, actualResultJavaType ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java index 1bfc7978ec..6740f0ce87 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java @@ -64,6 +64,7 @@ import org.hibernate.sql.results.spi.ScrollableResultsConsumer; import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; @@ -565,14 +566,14 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { public BasicType resolveType( int position, JavaType explicitJavaType, - SessionFactoryImplementor sessionFactory) { + TypeConfiguration typeConfiguration) { if ( columnNames == null ) { initializeArrays(); } final BasicType basicType = resultSetAccess.resolveType( position, explicitJavaType, - sessionFactory + typeConfiguration ); types[position - 1] = basicType; return basicType; @@ -622,7 +623,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { public BasicType resolveType( int position, JavaType explicitJavaType, - SessionFactoryImplementor sessionFactory) { + TypeConfiguration typeConfiguration) { final BasicType type = types[position - 1]; if ( type == null ) { throw new IllegalStateException( "Unexpected resolving of unavailable column at position: " + position ); @@ -632,7 +633,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { return (BasicType) type; } else { - return sessionFactory.getTypeConfiguration().getBasicTypeRegistry().resolve( + return typeConfiguration.getBasicTypeRegistry().resolve( explicitJavaType, type.getJdbcType() ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/ResultSetAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/ResultSetAccess.java index b03de27b3f..e4cc70cb14 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/ResultSetAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/ResultSetAccess.java @@ -10,8 +10,6 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import jakarta.persistence.EnumType; - import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -22,6 +20,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.spi.TypeConfiguration; +import jakarta.persistence.EnumType; + /** * Access to a JDBC ResultSet and information about it. * @@ -72,11 +72,7 @@ public interface ResultSetAccess extends JdbcValuesMetadata { } @Override - default BasicType resolveType( - int position, - JavaType explicitJavaType, - SessionFactoryImplementor sessionFactory) { - final TypeConfiguration typeConfiguration = getFactory().getTypeConfiguration(); + default BasicType resolveType(int position, JavaType explicitJavaType, TypeConfiguration typeConfiguration) { final JdbcServices jdbcServices = getFactory().getJdbcServices(); try { final ResultSetMetaData metaData = getResultSet().getMetaData(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/spi/JdbcValuesMetadata.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/spi/JdbcValuesMetadata.java index 299da05be5..983ec87622 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/spi/JdbcValuesMetadata.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/spi/JdbcValuesMetadata.java @@ -9,6 +9,7 @@ package org.hibernate.sql.results.jdbc.spi; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; /** * Access to information about the underlying JDBC values @@ -31,11 +32,20 @@ public interface JdbcValuesMetadata { String resolveColumnName(int position); /** - * The basic type of a particular result value by position + * Determine the mapping to use for a particular position in the result + */ + default BasicType resolveType( + int position, + JavaType explicitJavaType, + SessionFactoryImplementor sessionFactory) { + return resolveType( position, explicitJavaType, sessionFactory.getTypeConfiguration() ); + } + + /** + * Determine the mapping to use for a particular position in the result */ BasicType resolveType( int position, JavaType explicitJavaType, - SessionFactoryImplementor sessionFactory); - + TypeConfiguration typeConfiguration); }