HHH-15133 - Use specified result-type to better infer "shape" of query results with implicit selections

This commit is contained in:
Steve Ebersole 2022-03-29 11:29:05 -05:00
parent cac18ae0c7
commit bec32ebbc4
28 changed files with 763 additions and 107 deletions

View File

@ -2447,7 +2447,7 @@ public class HQLTest extends BaseEntityManagerFunctionalTestCase {
Object[].class)
.getResultList();
//end::hql-relational-comparisons-example[]
assertEquals(2, phonePayments.size());
assertEquals(3, phonePayments.size());
});
}

View File

@ -14,6 +14,7 @@ import jakarta.persistence.Tuple;
import jakarta.persistence.TupleElement;
import org.hibernate.HibernateException;
import org.hibernate.query.TypedTupleTransformer;
import org.hibernate.transform.ResultTransformer;
/**
@ -21,13 +22,18 @@ import org.hibernate.transform.ResultTransformer;
*
* @author Arnold Galovics
*/
public class NativeQueryTupleTransformer implements ResultTransformer<Tuple> {
public class NativeQueryTupleTransformer implements ResultTransformer<Tuple>, TypedTupleTransformer<Tuple> {
@Override
public Tuple transformTuple(Object[] tuple, String[] aliases) {
return new NativeTupleImpl( tuple, aliases );
}
@Override
public Class<Tuple> getTransformedType() {
return Tuple.class;
}
private static class NativeTupleElementImpl<X> implements TupleElement<X> {
private final Class<? extends X> javaType;

View File

@ -211,6 +211,11 @@ public class LoaderSqlAstCreationState
return null;
}
@Override
public Boolean isDeDuplicationEnabled() {
return false;
}
@Override
public Boolean isResultCachingEnabled() {
return false;

View File

@ -0,0 +1,22 @@
/*
* 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;
/**
* Extension to TupleTransformer exposing the transformation target type.
*
* @apiNote This is mainly intended for use in equality checking while applying
* result de-duplication for queries.
*
* @author Steve Ebersole
*/
public interface TypedTupleTransformer<T> extends TupleTransformer<T> {
/**
* The type resulting from this transformation
*/
Class<T> getTransformedType();
}

View File

@ -17,9 +17,9 @@ import org.hibernate.LockOptions;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.ResultListTransformer;
import org.hibernate.query.TupleTransformer;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.spi.MutableQueryOptions;
/**
@ -43,6 +43,7 @@ public class QueryOptionsImpl implements MutableQueryOptions, AppliedGraph {
private TupleTransformer tupleTransformer;
private ResultListTransformer resultListTransformer;
private Boolean deDupEnabled;
private RootGraphImplementor<?> rootGraph;
private GraphSemantic graphSemantic;
@ -160,6 +161,15 @@ public class QueryOptionsImpl implements MutableQueryOptions, AppliedGraph {
return resultListTransformer;
}
@Override
public Boolean isDeDuplicationEnabled() {
return deDupEnabled;
}
public void setDeDuplicationEnabled(boolean enabled) {
this.deDupEnabled = enabled;
}
public void setResultCacheRegionName(String resultCacheRegionName) {
this.resultCacheRegionName = resultCacheRegionName;
}

View File

@ -58,6 +58,11 @@ public class DelegatingQueryOptions implements QueryOptions {
return queryOptions.getResultListTransformer();
}
@Override
public Boolean isDeDuplicationEnabled() {
return queryOptions.isDeDuplicationEnabled();
}
@Override
public Boolean isResultCachingEnabled() {
return queryOptions.isResultCachingEnabled();

View File

@ -62,6 +62,13 @@ public interface QueryOptions {
*/
ResultListTransformer<?> getResultListTransformer();
Boolean isDeDuplicationEnabled();
default boolean shouldApplyDeDuplication() {
final Boolean setting = isDeDuplicationEnabled();
return setting != null && setting;
}
/**
* Should results from the query be cached?
*

View File

@ -80,6 +80,11 @@ public abstract class QueryOptionsAdapter implements QueryOptions {
return null;
}
@Override
public Boolean isDeDuplicationEnabled() {
return null;
}
@Override
public String getResultCacheRegionName() {
return null;

View File

@ -105,7 +105,7 @@ public class NativeSelectQueryPlanImpl<R> implements NativeSelectQueryPlan<R> {
jdbcParameterBindings,
SqmJdbcExecutionContextAdapter.usingLockingAndPaging( executionContext ),
null,
ListResultsConsumer.UniqueSemantic.NONE
ListResultsConsumer.UniqueSemantic.NEVER
);
}

View File

@ -122,7 +122,9 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
}
},
rowTransformer,
ListResultsConsumer.UniqueSemantic.FILTER
queryOptions.shouldApplyDeDuplication()
? ListResultsConsumer.UniqueSemantic.FILTER
: ListResultsConsumer.UniqueSemantic.NONE
);
}
finally {
@ -205,13 +207,7 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
// NOTE : if we get here we have a resultType of some kind
if ( queryOptions.getTupleTransformer() != null ) {
// aside from checking the type parameters for the given TupleTransformer
// there is not a decent way to verify that the TupleTransformer returns
// the same type. We rely on the API here and assume the best
return makeRowTransformerTupleTransformerAdapter( sqm, queryOptions );
}
else if ( selections.size() > 1 ) {
if ( selections.size() > 1 ) {
throw new IllegalQueryOperationException( "Query defined multiple selections, return cannot be typed (other that Object[] or Tuple)" );
}
else {

View File

@ -230,6 +230,7 @@ public class OutputsImpl implements Outputs {
executionContext,
null,
RowTransformerStandardImpl.INSTANCE,
null,
jdbcValues
);

View File

@ -68,16 +68,12 @@ import org.hibernate.type.BasicType;
import org.hibernate.type.descriptor.java.JavaType;
/**
* Standard JdbcSelectExecutor implementation used by Hibernate,
* through {@link JdbcSelectExecutorStandardImpl#INSTANCE}
*
* @author Steve Ebersole
*/
public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
// todo (6.0) : Make resolving these executors swappable - JdbcServices?
// Since JdbcServices is just a "composition service", this is actually
// a very good option...
// todo (6.0) : where do affected-table-names get checked for up-to-date?
// who is responsible for that? Here?
/**
* Singleton access
*/
@ -89,6 +85,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Class<R> domainResultType,
ListResultsConsumer.UniqueSemantic uniqueSemantic) {
// Only do auto flushing for top level queries
return executeQuery(
@ -96,6 +93,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcParameterBindings,
executionContext,
rowTransformer,
domainResultType,
(sql) -> executionContext.getSession()
.getJdbcCoordinator()
.getStatementPreparer()
@ -118,6 +116,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcParameterBindings,
executionContext,
rowTransformer,
null,
(sql) -> executionContext.getSession().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement(
sql,
false,
@ -152,6 +151,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Class<R> domainResultType,
Function<String, PreparedStatement> statementCreator,
ResultsConsumer<T, R> resultsConsumer) {
final PersistenceContext persistenceContext = executionContext.getSession().getPersistenceContext();
@ -168,6 +168,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcParameterBindings,
executionContext,
rowTransformer,
domainResultType,
statementCreator,
resultsConsumer
);
@ -184,6 +185,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Class<R> domainResultType,
Function<String, PreparedStatement> statementCreator,
ResultsConsumer<T, R> resultsConsumer) {
return doExecuteQuery(
@ -191,6 +193,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcParameterBindings,
getScrollContext( executionContext, executionContext.getSession().getPersistenceContext() ),
rowTransformer,
domainResultType,
statementCreator,
resultsConsumer
);
@ -214,6 +217,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
final AppliedGraph appliedGraph = queryOptions.getAppliedGraph();
final TupleTransformer<?> tupleTransformer = queryOptions.getTupleTransformer();
final ResultListTransformer<?> resultListTransformer = queryOptions.getResultListTransformer();
final Boolean deDuplicationEnabled = queryOptions.isDeDuplicationEnabled();
final Boolean resultCachingEnabled = queryOptions.isResultCachingEnabled();
final CacheRetrieveMode cacheRetrieveMode = queryOptions.getCacheRetrieveMode();
final CacheStoreMode cacheStoreMode = queryOptions.getCacheStoreMode();
@ -259,6 +263,11 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
return resultListTransformer;
}
@Override
public Boolean isDeDuplicationEnabled() {
return deDuplicationEnabled;
}
@Override
public Boolean isResultCachingEnabled() {
return resultCachingEnabled;
@ -322,11 +331,13 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
}
};
}
private <T, R> T doExecuteQuery(
JdbcSelect jdbcSelect,
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Class<R> domainResultType,
Function<String, PreparedStatement> statementCreator,
ResultsConsumer<T, R> resultsConsumer) {
@ -346,7 +357,10 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
if ( rowTransformer == null ) {
@SuppressWarnings("unchecked")
final TupleTransformer<R> tupleTransformer = (TupleTransformer<R>) executionContext.getQueryOptions().getTupleTransformer();
final TupleTransformer<R> tupleTransformer = (TupleTransformer<R>) executionContext
.getQueryOptions()
.getTupleTransformer();
if ( tupleTransformer == null ) {
rowTransformer = RowTransformerStandardImpl.instance();
}
@ -415,6 +429,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
? LockOptions.NONE
: executionContext.getQueryOptions().getLockOptions(),
rowTransformer,
domainResultType,
jdbcValues
);

View File

@ -22,11 +22,21 @@ import org.hibernate.sql.results.spi.RowTransformer;
*/
@Incubating
public interface JdbcSelectExecutor {
default <R> List<R> list(
JdbcSelect jdbcSelect,
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
ListResultsConsumer.UniqueSemantic uniqueSemantic) {
return list( jdbcSelect, jdbcParameterBindings, executionContext, rowTransformer, null, uniqueSemantic );
}
<R> List<R> list(
JdbcSelect jdbcSelect,
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Class<R> requestedJavaType,
ListResultsConsumer.UniqueSemantic uniqueSemantic);
<R> ScrollableResultsImplementor<R> scroll(

View File

@ -57,10 +57,12 @@ public class ResultsHelper {
ExecutionContext executionContext,
LockOptions lockOptions,
RowTransformer<R> rowTransformer,
Class<R> transformedResultJavaType,
JdbcValues jdbcValues) {
final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory();
final Map<NavigablePath, Initializer> initializerMap = new LinkedHashMap<>();
final List<Initializer> initializers = new ArrayList<>();
final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory();
final List<DomainResultAssembler<?>> assemblers = jdbcValues.getValuesMapping().resolveAssemblers(
new AssemblerCreationState() {
@ -107,7 +109,7 @@ public class ResultsHelper {
logInitializers( initializerMap );
return new StandardRowReader<>( assemblers, initializers, rowTransformer );
return new StandardRowReader<>( assemblers, initializers, rowTransformer, transformedResultJavaType );
}
private static void logInitializers(Map<NavigablePath, Initializer> initializerMap) {

View File

@ -32,18 +32,21 @@ public class StandardRowReader<T> implements RowReader<T> {
private final List<DomainResultAssembler<?>> resultAssemblers;
private final List<Initializer> initializers;
private final RowTransformer<T> rowTransformer;
private final Class<T> domainResultJavaType;
private final int assemblerCount;
public StandardRowReader(
List<DomainResultAssembler<?>> resultAssemblers,
List<Initializer> initializers,
RowTransformer<T> rowTransformer) {
RowTransformer<T> rowTransformer,
Class<T> domainResultJavaType) {
this.resultAssemblers = resultAssemblers;
this.initializers = initializers;
this.rowTransformer = rowTransformer;
this.assemblerCount = resultAssemblers.size();
this.domainResultJavaType = domainResultJavaType;
logDebugInfo();
}
@ -61,18 +64,22 @@ public class StandardRowReader<T> implements RowReader<T> {
}
@Override
@SuppressWarnings("unchecked")
public Class<T> getResultJavaType() {
if ( resultAssemblers.size() == 1 ) {
return (Class<T>) resultAssemblers.get( 0 ).getAssembledJavaType().getJavaTypeClass();
}
return (Class<T>) Object[].class;
public Class<T> getDomainResultResultJavaType() {
return domainResultJavaType;
}
@Override
public List<JavaType> getResultJavaTypes() {
List<JavaType> javaTypes = new ArrayList<>( resultAssemblers.size() );
public Class<?> getResultJavaType() {
if ( resultAssemblers.size() == 1 ) {
return resultAssemblers.get( 0 ).getAssembledJavaType().getJavaTypeClass();
}
return Object[].class;
}
@Override
public List<JavaType<?>> getResultJavaTypes() {
List<JavaType<?>> javaTypes = new ArrayList<>( resultAssemblers.size() );
for ( DomainResultAssembler resultAssembler : resultAssemblers ) {
javaTypes.add( resultAssembler.getAssembledJavaType() );
}

View File

@ -8,50 +8,94 @@ package org.hibernate.sql.results.spi;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.query.ResultListTransformer;
import org.hibernate.query.TupleTransformer;
import org.hibernate.query.TypedTupleTransformer;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl;
import org.hibernate.sql.results.jdbc.spi.JdbcValues;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl;
import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.spi.EntityJavaType;
import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry;
import org.hibernate.type.spi.TypeConfiguration;
/**
* ResultsConsumer for creating a List of results
*
* @author Steve Ebersole
*/
public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
/**
* Singleton access
*/
private static final ListResultsConsumer UNIQUE_FILTER_INSTANCE = new ListResultsConsumer( UniqueSemantic.FILTER );
private static final ListResultsConsumer NORMAL_INSTANCE = new ListResultsConsumer( UniqueSemantic.NONE );
private static final ListResultsConsumer UNIQUE_INSTANCE = new ListResultsConsumer( UniqueSemantic.ASSERT );
private static final ListResultsConsumer<?> NEVER_DE_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.NEVER );
private static final ListResultsConsumer<?> IGNORE_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.NONE );
private static final ListResultsConsumer<?> DE_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.FILTER );
private static final ListResultsConsumer<?> ERROR_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.ASSERT );
@SuppressWarnings("unchecked")
public static <R> ListResultsConsumer<R> instance(UniqueSemantic uniqueSemantic) {
switch ( uniqueSemantic ) {
case ASSERT:
return UNIQUE_INSTANCE;
case FILTER:
return UNIQUE_FILTER_INSTANCE;
default:
return NORMAL_INSTANCE;
case ASSERT: {
return (ListResultsConsumer<R>) ERROR_DUP_CONSUMER;
}
case FILTER: {
return (ListResultsConsumer<R>) DE_DUP_CONSUMER;
}
case NEVER: {
return (ListResultsConsumer<R>) NEVER_DE_DUP_CONSUMER;
}
default: {
return (ListResultsConsumer<R>) IGNORE_DUP_CONSUMER;
}
}
}
/**
* Ways this consumer can handle in-memory row de-duplication
*/
public enum UniqueSemantic {
/**
* Apply no in-memory de-duplication
*/
NONE,
/**
* Apply in-memory de-duplication, removing rows already part of the results
*/
FILTER,
ASSERT;
/**
* Apply in-memory duplication checks, throwing a HibernateException when duplicates are found
*/
ASSERT,
/**
* Never apply unique handling. E.g. for NativeQuery. Whereas {@link #NONE} can be adjusted,
* NEVER will never apply unique handling
*/
NEVER
}
private final UniqueSemantic uniqueSemantic;
private final ResultHandler<R> resultHandler;
public ListResultsConsumer(UniqueSemantic uniqueSemantic) {
this.uniqueSemantic = uniqueSemantic;
if ( uniqueSemantic == UniqueSemantic.FILTER ) {
resultHandler = ListResultsConsumer::deDuplicationHandling;
}
else if ( uniqueSemantic == UniqueSemantic.ASSERT ) {
resultHandler = ListResultsConsumer::duplicationErrorHandling;
}
else {
resultHandler = ListResultsConsumer::applyAll;
}
}
@Override
@ -63,61 +107,75 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
RowProcessingStateStandardImpl rowProcessingState,
RowReader<R> rowReader) {
final PersistenceContext persistenceContext = session.getPersistenceContext();
final TypeConfiguration typeConfiguration = session.getTypeConfiguration();
final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry();
final QueryOptions queryOptions = rowProcessingState.getQueryOptions();
RuntimeException ex = null;
try {
persistenceContext.getLoadContexts().register( jdbcValuesSourceProcessingState );
final List<R> results = new ArrayList<>();
boolean uniqueRows = false;
final JavaType<R> domainResultJavaType;
final ResultHandler<R> resultHandlerToUse;
if ( uniqueSemantic != UniqueSemantic.NONE ) {
final Class<R> resultJavaType = rowReader.getResultJavaType();
if ( resultJavaType != null && !resultJavaType.isArray() ) {
final EntityPersister entityDescriptor = session.getFactory()
.getRuntimeMetamodels()
.getMappingMetamodel()
.findEntityDescriptor( resultJavaType );
if ( entityDescriptor != null ) {
uniqueRows = true;
}
}
}
final TupleTransformer<?> tupleTransformer = queryOptions.getTupleTransformer();
if ( tupleTransformer instanceof TypedTupleTransformer ) {
//noinspection unchecked
final TypedTupleTransformer<R> typedTupleTransformer = (TypedTupleTransformer<R>) tupleTransformer;
domainResultJavaType = javaTypeRegistry.resolveDescriptor( typedTupleTransformer.getTransformedType() );
if ( uniqueRows ) {
final List<JavaType> resultJavaTypes = rowReader.getResultJavaTypes();
assert resultJavaTypes.size() == 1;
final JavaType<R> resultJavaType = resultJavaTypes.get( 0 );
while ( rowProcessingState.next() ) {
final R row = rowReader.readRow( rowProcessingState, processingOptions );
boolean add = true;
for ( R existingRow : results ) {
if ( resultJavaType.areEqual( existingRow, row ) ) {
if ( uniqueSemantic == UniqueSemantic.ASSERT && !rowProcessingState.hasCollectionInitializers() ) {
throw new HibernateException(
"More than one row with the given identifier was found: " +
jdbcValuesSourceProcessingState.getExecutionContext()
.getEntityId() +
", for class: " +
rowReader.getResultJavaType().getName()
);
if ( uniqueSemantic == UniqueSemantic.NEVER ) {
resultHandlerToUse = this.resultHandler;
}
add = false;
break;
else if ( queryOptions.shouldApplyDeDuplication() ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
}
else if ( domainResultJavaType instanceof EntityJavaType ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
}
if ( add ) {
results.add( row );
}
rowProcessingState.finishRowProcessing();
else {
resultHandlerToUse = this.resultHandler;
}
}
else {
domainResultJavaType = resolveDomainResultJavaType(
rowReader.getDomainResultResultJavaType(),
rowReader.getResultJavaTypes(),
typeConfiguration
);
if ( uniqueSemantic == UniqueSemantic.NEVER ) {
resultHandlerToUse = this.resultHandler;
}
else if ( queryOptions.shouldApplyDeDuplication() ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
}
else {
if ( uniqueSemantic == UniqueSemantic.ASSERT ) {
if ( rowProcessingState.hasCollectionInitializers ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
}
else {
resultHandlerToUse = this.resultHandler;
}
}
else if ( domainResultJavaType instanceof EntityJavaType ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
}
else {
resultHandlerToUse = this.resultHandler;
}
}
}
while ( rowProcessingState.next() ) {
results.add( rowReader.readRow( rowProcessingState, processingOptions ) );
final R row = rowReader.readRow( rowProcessingState, processingOptions );
resultHandlerToUse.handle( row, domainResultJavaType, results, rowProcessingState );
rowProcessingState.finishRowProcessing();
}
}
try {
jdbcValuesSourceProcessingState.finishUp();
}
@ -126,12 +184,11 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
}
//noinspection unchecked
final ResultListTransformer<R> resultListTransformer = (ResultListTransformer<R>) jdbcValuesSourceProcessingState.getExecutionContext()
.getQueryOptions()
.getResultListTransformer();
final ResultListTransformer<R> resultListTransformer = (ResultListTransformer<R>) queryOptions.getResultListTransformer();
if ( resultListTransformer != null ) {
return resultListTransformer.transformList( results );
}
return results;
}
catch (RuntimeException e) {
@ -160,8 +217,108 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
throw new IllegalStateException( "Should not reach this!" );
}
/**
* Essentially a tri-consumer for applying the different duplication strategies.
*
* @see UniqueSemantic
*/
@FunctionalInterface
private interface ResultHandler<R> {
void handle(R result, JavaType<R> transformedJavaType, List<R> results, RowProcessingStateStandardImpl rowProcessingState);
}
public static <R> void deDuplicationHandling(
R result,
JavaType<R> transformedJavaType,
List<R> results,
RowProcessingStateStandardImpl rowProcessingState) {
withDuplicationCheck(
result,
transformedJavaType,
results,
rowProcessingState,
false
);
}
private static <R> void withDuplicationCheck(
R result,
JavaType<R> transformedJavaType,
List<R> results,
RowProcessingStateStandardImpl rowProcessingState,
boolean throwException) {
boolean addResult = true;
for ( int i = 0; i < results.size(); i++ ) {
final R existingResult = results.get( i );
if ( transformedJavaType.areEqual( result, existingResult ) ) {
if ( throwException && ! rowProcessingState.hasCollectionInitializers ) {
throw new HibernateException(
String.format(
Locale.ROOT,
"Duplicate row was found and `%s` was specified",
UniqueSemantic.ASSERT
)
);
}
addResult = false;
break;
}
}
if ( addResult ) {
results.add( result );
}
}
public static <R> void duplicationErrorHandling(
R result,
JavaType<R> transformedJavaType,
List<R> results,
RowProcessingStateStandardImpl rowProcessingState) {
withDuplicationCheck(
result,
transformedJavaType,
results,
rowProcessingState,
true
);
}
public static <R> void applyAll(
R result,
JavaType<R> transformedJavaType,
List<R> results,
RowProcessingStateStandardImpl rowProcessingState) {
results.add( result );
}
private JavaType<R> resolveDomainResultJavaType(
Class<R> domainResultResultJavaType,
List<JavaType<?>> resultJavaTypes,
TypeConfiguration typeConfiguration) {
final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry();
if ( domainResultResultJavaType != null ) {
return javaTypeRegistry.resolveDescriptor( domainResultResultJavaType );
}
if ( resultJavaTypes.size() == 1 ) {
//noinspection unchecked
return (JavaType<R>) resultJavaTypes.get( 0 );
}
return javaTypeRegistry.resolveDescriptor( Object[].class );
}
@Override
public boolean canResultsBeCached() {
return true;
}
@Override
public String toString() {
return "ListResultsConsumer(" + uniqueSemantic + ")";
}
}

View File

@ -13,6 +13,9 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValues;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
/**
* Consumes {@link JdbcValues} and returns the consumed values in whatever form this
* consumer returns, generally a {@link java.util.List} or a {@link org.hibernate.ScrollableResults}
*
* @author Steve Ebersole
*/
public interface ResultsConsumer<T, R> {

View File

@ -23,27 +23,38 @@ import org.hibernate.type.descriptor.java.JavaType;
*/
public interface RowReader<R> {
/**
* The overall row result Java type. Might be a scalar type, an
* entity type, etc. Might also be a `Object[].class` for multiple
* results (domain selections).
* The type actually returned from this reader's {@link #readRow} call,
* accounting for any transformers.
* <p/>
* May be null to indicate that no transformation is applied.
* <p/>
* Ultimately intended for use in comparing values are being de-duplicated
*/
Class<R> getResultJavaType();
Class<R> getDomainResultResultJavaType();
/**
* The JavaTypes of the result
* The row result Java type, before any transformations.
*
* @apiNote along with {@link #getResultJavaTypes()}, describes the "raw"
* values as determined from the {@link org.hibernate.sql.results.graph.DomainResult}
* references associated with the JdbcValues being processed
*/
List<JavaType> getResultJavaTypes();
Class<?> getResultJavaType();
/**
* The initializers associated with this reader
* The individual JavaType for each DomainResult
*/
List<JavaType<?>> getResultJavaTypes();
/**
* The initializers associated with this reader.
*
* @see org.hibernate.sql.results.graph.DomainResult
*/
List<Initializer> getInitializers();
/**
* The actual coordination of reading a row
*
* todo (6.0) : JdbcValuesSourceProcessingOptions is available through RowProcessingState - why pass it in separately
* should use one approach or the other
*/
R readRow(RowProcessingState processingState, JdbcValuesSourceProcessingOptions options);

View File

@ -9,11 +9,12 @@ package org.hibernate.transform;
import java.lang.reflect.Constructor;
import org.hibernate.QueryException;
import org.hibernate.query.TypedTupleTransformer;
/**
* Wraps the tuples in a constructor call.
*/
public class AliasToBeanConstructorResultTransformer<T> implements ResultTransformer<T> {
public class AliasToBeanConstructorResultTransformer<T> implements ResultTransformer<T>, TypedTupleTransformer<T> {
private final Constructor<T> constructor;
@ -26,6 +27,11 @@ public class AliasToBeanConstructorResultTransformer<T> implements ResultTransfo
this.constructor = constructor;
}
@Override
public Class<T> getTransformedType() {
return constructor.getDeclaringClass();
}
/**
* Wrap the incoming tuples in a call to our configured constructor.
*/

View File

@ -14,6 +14,7 @@ import org.hibernate.property.access.internal.PropertyAccessStrategyChainedImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyFieldImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyMapImpl;
import org.hibernate.property.access.spi.Setter;
import org.hibernate.query.TypedTupleTransformer;
/**
* Result transformer that allows to transform a result to
@ -22,7 +23,7 @@ import org.hibernate.property.access.spi.Setter;
*
* @author max
*/
public class AliasToBeanResultTransformer<T> implements ResultTransformer<T> {
public class AliasToBeanResultTransformer<T> implements ResultTransformer<T>, TypedTupleTransformer<T> {
// IMPL NOTE : due to the delayed population of setters (setters cached
// for performance), we really cannot properly define equality for
@ -41,6 +42,11 @@ public class AliasToBeanResultTransformer<T> implements ResultTransformer<T> {
this.resultClass = resultClass;
}
@Override
public Class<T> getTransformedType() {
return resultClass;
}
@Override
public T transformTuple(Object[] tuple, String[] aliases) {
T result;

View File

@ -8,6 +8,7 @@ package org.hibernate.transform;
import java.util.Map;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.query.TypedTupleTransformer;
/**
* {@link ResultTransformer} implementation which builds a map for each "row",
@ -16,7 +17,7 @@ import org.hibernate.internal.util.collections.CollectionHelper;
* @author Gavin King
* @author Steve Ebersole
*/
public class AliasToEntityMapResultTransformer implements ResultTransformer<Map<String,Object>> {
public class AliasToEntityMapResultTransformer implements ResultTransformer<Map<String,Object>>, TypedTupleTransformer<Map<String,Object>> {
public static final AliasToEntityMapResultTransformer INSTANCE = new AliasToEntityMapResultTransformer();
@ -26,6 +27,12 @@ public class AliasToEntityMapResultTransformer implements ResultTransformer<Map<
private AliasToEntityMapResultTransformer() {
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Class getTransformedType() {
return Map.class;
}
@Override
public Map<String,Object> transformTuple(Object[] tuple, String[] aliases) {
Map<String,Object> result = CollectionHelper.mapOfSize( tuple.length );

View File

@ -9,10 +9,12 @@ package org.hibernate.transform;
import java.util.Arrays;
import java.util.List;
import org.hibernate.query.TypedTupleTransformer;
/**
* Transforms each result row from a tuple into a {@link List} whose elements are each tuple value
*/
public class ToListResultTransformer implements ResultTransformer<List<Object>> {
public class ToListResultTransformer implements ResultTransformer<List<Object>>, TypedTupleTransformer<List<Object>> {
public static final ToListResultTransformer INSTANCE = new ToListResultTransformer();
/**
@ -21,6 +23,12 @@ public class ToListResultTransformer implements ResultTransformer<List<Object>>
private ToListResultTransformer() {
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class<List<Object>> getTransformedType() {
return (Class) List.class;
}
@Override
public List<Object> transformTuple(Object[] tuple, String[] aliases) {
return Arrays.asList( tuple );

View File

@ -16,6 +16,7 @@ import org.hibernate.testing.orm.domain.StandardDomainModel;
import org.hibernate.testing.orm.domain.retail.Product;
import org.hibernate.testing.orm.domain.retail.Vendor;
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;
@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@DomainModel( standardModels = StandardDomainModel.RETAIL )
@SessionFactory
@JiraKey( "HHH-15133" )
public class ImplicitSelectWithJoinTests {
private static final String HQL = "from Product p join p.vendor v where v.name like '%Steve%'";
private static final String HQL2 = "select p " + HQL;

View File

@ -0,0 +1,279 @@
/*
* 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.orm.test.sql.results;
import java.util.List;
import java.util.UUID;
import javax.money.Monetary;
import javax.money.MonetaryAmount;
import jakarta.persistence.Tuple;
import org.hibernate.query.TypedTupleTransformer;
import org.hibernate.testing.orm.domain.StandardDomainModel;
import org.hibernate.testing.orm.domain.retail.CardPayment;
import org.hibernate.testing.orm.domain.retail.LineItem;
import org.hibernate.testing.orm.domain.retail.Name;
import org.hibernate.testing.orm.domain.retail.Order;
import org.hibernate.testing.orm.domain.retail.Product;
import org.hibernate.testing.orm.domain.retail.SalesAssociate;
import org.hibernate.testing.orm.domain.retail.Vendor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Steve Ebersole
*/
@DomainModel( standardModels = StandardDomainModel.RETAIL )
@SessionFactory
@Jira( "https://hibernate.atlassian.net/browse/HHH-15133" )
public class ResultsShapeTests {
@Test
public void testSimpleEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final List<?> orders = session.createQuery( "select o from Order o" ).list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@Test
public void testTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final List<Order> orders = session.createQuery( "select o from Order o", Order.class ).list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@Test
public void testArrayTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final List<Object[]> orders = session.createQuery( "select o from Order o", Object[].class ).list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Object[].class );
assertThat( orders.get( 1 ) ).isInstanceOf( Object[].class );
} );
}
@Test
public void testTupleTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final List<Tuple> orders = session.createQuery( "select o from Order o", Tuple.class ).list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Tuple.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Tuple.class );
} );
}
@Test
public void testDuplicatedTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from LineItem i join i.order o";
final List<Order> orders = session.createQuery( hql, Order.class ).list();
// because we select the entity and specify it as the result type, the results are de-duped
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@Test
public void testDuplicatedArrayTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from LineItem i join i.order o";
final List<Object[]> orders = session.createQuery( hql, Object[].class ).list();
// because we select the entity again, but here specify the full array as the result type - the results are not de-duped
assertThat( orders ).hasSize( 3 );
assertThat( orders.get( 0 ) ).isInstanceOf( Object[].class );
assertThat( orders.get( 1 ) ).isInstanceOf( Object[].class );
} );
}
@Test
public void testDuplicatedTupleTypedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from LineItem i join i.order o";
final List<Tuple> orders = session.createQuery( hql, Tuple.class ).list();
// Tuple is a special case or Object[] - not de-duped
assertThat( orders ).hasSize( 3 );
assertThat( orders.get( 0 ) ).isInstanceOf( Tuple.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Tuple.class );
} );
}
@Test
public void testTupleTransformedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from Order o";
final List<Order> orders = session.createQuery( hql, Order.class )
.setTupleTransformer( (tuple, aliases) -> (Order) tuple[ 0 ] )
.list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
scope.inTransaction( (session) -> {
final String hql = "select o from Order o";
final List<Order> orders = session.createQuery( hql )
.setTupleTransformer( (tuple, aliases) -> tuple[ 0 ] )
.list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@Test
public void testDuplicatedTupleTransformedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from LineItem i join i.order o";
final List<Order> orders = session.createQuery( hql, Order.class )
.setTupleTransformer( (tuple, aliases) -> (Order) tuple[ 0 ] )
.list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
scope.inTransaction( (session) -> {
final String hql = "select o from LineItem i join i.order o";
final List<Order> orders = session.createQuery( hql )
.setTupleTransformer( (tuple, aliases) -> tuple[ 0 ] )
.list();
// only 2 orders
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
private final TypedTupleTransformer<Order> ORDER_TUPLE_TRANSFORMER = new TypedTupleTransformer<>() {
@Override
public Class<Order> getTransformedType() {
return Order.class;
}
@Override
public Order transformTuple(Object[] tuple, String[] aliases) {
return (Order) tuple[0];
}
};
@Test
public void testTypedTupleTransformedEntitySelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select o from Order o";
final List<Order> orders = session.createQuery( hql, Order.class )
.setTupleTransformer( ORDER_TUPLE_TRANSFORMER )
.list();
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
scope.inTransaction( (session) -> {
final String hql = "select o from Order o";
final List<Order> orders = session.createQuery( hql )
.setTupleTransformer( ORDER_TUPLE_TRANSFORMER )
.list();
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@Test
public void testDuplicatedTypedTupleTransformedEntitySelection(SessionFactoryScope scope) {
final String hql = "select o from LineItem i join i.order o";
scope.inTransaction( (session) -> {
final List<Order> orders = session.createQuery( hql, Order.class )
.setTupleTransformer( (tuple, aliases) -> (Order) tuple[0] )
.list();
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
scope.inTransaction( (session) -> {
final List<Order> orders = session.createQuery( hql )
.setTupleTransformer( (tuple, aliases) -> tuple[ 0 ] )
.list();
assertThat( orders ).hasSize( 2 );
assertThat( orders.get( 0 ) ).isInstanceOf( Order.class );
assertThat( orders.get( 1 ) ).isInstanceOf( Order.class );
} );
}
@BeforeEach
public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Vendor acme = new Vendor( 1, "Acme, Inc.", null );
session.persist( acme );
final Product widget = new Product( 1, UUID.randomUUID(), acme );
session.persist( widget );
final SalesAssociate associate = new SalesAssociate( 1, new Name( "John", "Doe" ) );
session.persist( associate );
final MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory()
.setNumber( 1 )
.setCurrency( "USD" )
.create();
final CardPayment payment1 = new CardPayment( 1, 123, oneDollar );
session.persist( payment1 );
final Order order1 = new Order( 1, payment1, associate );
session.persist( order1 );
final LineItem lineItem11 = new LineItem( 11, widget, 1, oneDollar, order1 );
session.persist( lineItem11 );
final LineItem lineItem12 = new LineItem( 12, widget, 1, oneDollar, order1 );
session.persist( lineItem12 );
final CardPayment payment2 = new CardPayment( 2, 321, oneDollar );
session.persist( payment2 );
final Order order2 = new Order( 2, payment2, associate );
session.persist( order2 );
final LineItem lineItem2 = new LineItem( 2, widget, 1, oneDollar, order2 );
session.persist( lineItem2 );
} );
}
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete LineItem" ).executeUpdate();
session.createMutationQuery( "delete Order" ).executeUpdate();
session.createMutationQuery( "delete Payment" ).executeUpdate();
session.createMutationQuery( "delete Product" ).executeUpdate();
session.createMutationQuery( "delete Vendor" ).executeUpdate();
session.createMutationQuery( "delete SalesAssociate" ).executeUpdate();
} );
}
}

View File

@ -25,6 +25,22 @@ public class LineItem {
private Order order;
public LineItem() {
}
public LineItem(
Integer id,
Product product,
int quantity,
MonetaryAmount subTotal,
Order order) {
this.id = id;
this.product = product;
this.quantity = quantity;
this.subTotal = subTotal;
this.order = order;
}
@Id
public Integer getId() {
return id;

View File

@ -25,6 +25,20 @@ public class Order {
private Payment payment;
private SalesAssociate salesAssociate;
public Order() {
}
public Order(Integer id, Payment payment, SalesAssociate salesAssociate) {
this( id, Instant.now(), payment, salesAssociate );
}
public Order(Integer id, Instant transacted, Payment payment, SalesAssociate salesAssociate) {
this.id = id;
this.transacted = transacted;
this.payment = payment;
this.salesAssociate = salesAssociate;
}
@Id
public Integer getId() {
return id;

View File

@ -0,0 +1,31 @@
/*
* 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.testing.orm.junit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specifies the URL to the Jira issue associated with a test.
* Is repeatable, so multiple JIRA issues can be indicated.
*
* @see JiraGroup
*
* @author Steve Ebersole
*/
@Retention( RetentionPolicy.RUNTIME )
@Target({ElementType.TYPE, ElementType.METHOD})
@Repeatable( JiraGroup.class )
public @interface Jira {
/**
* The URL to the Jira issue
*/
String value();
}

View File

@ -0,0 +1,25 @@
/*
* 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.testing.orm.junit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Grouping annotation for `@Jira`
*
* @see Jira
*
* @author Steve Ebersole
*/
@Retention( RetentionPolicy.RUNTIME )
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface JiraGroup {
Jira[] value();
}