HHH-18629 Fix inconsistent column alias generated while result class is used for placeholder

This commit is contained in:
Christian Beikov 2024-11-21 16:44:28 +01:00
parent 800a3f0738
commit c5f5e10df4
4 changed files with 185 additions and 54 deletions

View File

@ -18,6 +18,7 @@ import java.util.UUID;
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 abstract class AbstractSharedSessionContract implements SharedSessionCont
// 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 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
@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 <T> void addResultType(Class<T> resultClass, NativeQueryImplementor<T> query) {
if ( Tuple.class.equals( resultClass ) ) {
query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE );

View File

@ -17,8 +17,12 @@ import java.util.Set;
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.Tuple;
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.internal.util.collections.CollectionHelper.makeCopy;
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<R>
private final List<ParameterOccurrence> parameterOccurrences;
private final QueryParameterBindings parameterBindings;
private final Class<R> resultType;
private final ResultSetMapping resultSetMapping;
private final boolean resultMappingSuppliedToCtor;
@ -166,6 +175,7 @@ public class NativeQueryImpl<R>
return false;
}
},
null,
session
);
}
@ -218,26 +228,9 @@ public class NativeQueryImpl<R>
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 class NativeQueryImpl<R>
mappingMemento.resolve( resultSetMapping, querySpaceConsumer, context );
return true;
},
null,
session
);
@ -268,6 +262,15 @@ public class NativeQueryImpl<R>
Supplier<ResultSetMapping> resultSetMappingCreator,
ResultSetMappingHandler resultSetMappingHandler,
SharedSessionContractImplementor session) {
this( memento, resultSetMappingCreator, resultSetMappingHandler, null, session );
}
public NativeQueryImpl(
NamedNativeQueryMemento<?> memento,
Supplier<ResultSetMapping> resultSetMappingCreator,
ResultSetMappingHandler resultSetMappingHandler,
@Nullable Class<R> resultType,
SharedSessionContractImplementor session) {
super( session );
this.originalSqlString = memento.getOriginalSqlString();
@ -279,6 +282,7 @@ public class NativeQueryImpl<R>
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 class NativeQueryImpl<R>
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 class NativeQueryImpl<R>
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 class NativeQueryImpl<R>
}
public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) {
this( sqlString, null, session );
}
public NativeQueryImpl(String sqlString, @Nullable Class<R> resultType, SharedSessionContractImplementor session) {
super( session );
this.querySpaces = new HashSet<>();
@ -320,11 +350,46 @@ public class NativeQueryImpl<R>
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 <T> void setTupleTransformerForResultType(Class<T> 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 <T> boolean hasJavaTypeDescriptor(Class<T> resultClass) {
final JavaType<Object> descriptor = getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass );
return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class;
}
@FunctionalInterface
private interface ResultSetMappingHandler {
boolean resolveResultSetMapping(
@ -436,11 +501,16 @@ public class NativeQueryImpl<R>
return getQueryParameterBindings();
}
@Override
public Class<R> 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 class NativeQueryImpl<R>
);
}
private Class<?> extractResultClass(ResultSetMapping resultSetMapping) {
private Class<R> extractResultClass(ResultSetMapping resultSetMapping) {
final List<ResultBuilder> 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<R>) resultBuilder.getJavaType();
}
}
return null;
@ -618,13 +688,29 @@ public class NativeQueryImpl<R>
}
protected SelectQueryPlan<R> 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<R> createQueryPlan() {
private NativeSelectQueryPlan<R> createQueryPlan(ResultSetMapping resultSetMapping) {
final NativeSelectQueryDefinition<R> queryDefinition = new NativeSelectQueryDefinition<>() {
final String sqlString = expandParameterLists();
@ -834,7 +920,7 @@ public class NativeQueryImpl<R>
return bindValueMaxCount;
}
private SelectInterpretationsKey selectInterpretationsKey() {
private SelectInterpretationsKey selectInterpretationsKey(ResultSetMapping resultSetMapping) {
return new SelectInterpretationsKey(
getQueryString(),
resultSetMapping,

View File

@ -18,6 +18,7 @@ import org.hibernate.dialect.SybaseDialect;
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.type.descriptor.jdbc.spi.JdbcTypeRegistry;
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 class NativeQueryResultBuilderTests {
);
}
@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 class NativeQueryResultBuilderTests {
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 class NativeQueryResultBuilderTests {
@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();
}
);
}

View File

@ -10,7 +10,6 @@ import org.hibernate.query.NativeQuery;
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 class EntityReturnClassTests {
@Test
@JiraKey("HHH-18864")
@NotImplementedYet
public void testAddEntityWithEntityReturn(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
NativeQuery<Speech> query = session.createNativeQuery( "select {s.*} from Speech s", Speech.class );
@ -75,7 +73,6 @@ public class EntityReturnClassTests {
@Test
@JiraKey("HHH-18864")
@NotImplementedYet
public void testAddRootWithEntityReturn(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
NativeQuery<Speech> query = session.createNativeQuery( "select {s.*} from Speech s", Speech.class );
@ -91,7 +88,6 @@ public class EntityReturnClassTests {
@Test
@JiraKey("HHH-18864")
@NotImplementedYet
public void testAddEntityWithInterfaceReturn(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
NativeQuery<SpeechInterface> query = session.createNativeQuery( "select {s.*} from Speech s", SpeechInterface.class );
@ -103,7 +99,6 @@ public class EntityReturnClassTests {
@Test
@JiraKey("HHH-18864")
@NotImplementedYet
public void testAddRootWithInterfaceReturn(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
NativeQuery<SpeechInterface> query = session.createNativeQuery( "select {s.*} from Speech s", SpeechInterface.class );