HHH-15930 - Support scalar resultClass in @NamedNativeQuery

This commit is contained in:
Steve Ebersole 2022-12-22 15:48:16 -06:00
parent 501d3869d4
commit 263768d5c5
8 changed files with 139 additions and 59 deletions

View File

@ -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[]

View File

@ -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<String> 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<Object[]> 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> 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<Person> 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<Object[]> 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<Object[]> tuples = entityManager.createNamedQuery(
"find_all_spaceships")
"find_all_spaceships", Object[].class)
.getResultList();
for(Object[] tuple : tuples) {

View File

@ -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(

View File

@ -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<String, String, DynamicFetchBuilderLegacy> legacyFetchResolver,
DomainResultCreationState domainResultCreationState) {
final MappingMetamodelImplementor mappingMetamodel = domainResultCreationState.getSqlAstCreationState()
.getCreationContext()
.getMappingMetamodel();
final TypeConfiguration typeConfiguration = mappingMetamodel.getTypeConfiguration();
final int jdbcResultPosition = resultPosition + 1;
final BasicType<Object> 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;
}
}

View File

@ -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<R>
if ( memento.getResultMappingClass() != null ) {
resultSetMapping.addResultBuilder(
Builders.implicitEntityResultBuilder(
resultClassBuilder(
memento.getResultMappingClass(),
context
)
@ -239,36 +239,18 @@ public class NativeQueryImpl<R>
}
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<R>
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 );

View File

@ -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 <J> BasicType<J> resolveType(
int position,
JavaType<J> explicitJavaType,
SessionFactoryImplementor sessionFactory) {
TypeConfiguration typeConfiguration) {
if ( columnNames == null ) {
initializeArrays();
}
final BasicType<J> basicType = resultSetAccess.resolveType(
position,
explicitJavaType,
sessionFactory
typeConfiguration
);
types[position - 1] = basicType;
return basicType;
@ -622,7 +623,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
public <J> BasicType<J> resolveType(
int position,
JavaType<J> 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<J>) type;
}
else {
return sessionFactory.getTypeConfiguration().getBasicTypeRegistry().resolve(
return typeConfiguration.getBasicTypeRegistry().resolve(
explicitJavaType,
type.getJdbcType()
);

View File

@ -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 <J> BasicType<J> resolveType(
int position,
JavaType<J> explicitJavaType,
SessionFactoryImplementor sessionFactory) {
final TypeConfiguration typeConfiguration = getFactory().getTypeConfiguration();
default <J> BasicType<J> resolveType(int position, JavaType<J> explicitJavaType, TypeConfiguration typeConfiguration) {
final JdbcServices jdbcServices = getFactory().getJdbcServices();
try {
final ResultSetMetaData metaData = getResultSet().getMetaData();

View File

@ -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 <J> BasicType<J> resolveType(
int position,
JavaType<J> explicitJavaType,
SessionFactoryImplementor sessionFactory) {
return resolveType( position, explicitJavaType, sessionFactory.getTypeConfiguration() );
}
/**
* Determine the mapping to use for a particular position in the result
*/
<J> BasicType<J> resolveType(
int position,
JavaType<J> explicitJavaType,
SessionFactoryImplementor sessionFactory);
TypeConfiguration typeConfiguration);
}