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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,7 +122,9 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
} }
}, },
rowTransformer, rowTransformer,
ListResultsConsumer.UniqueSemantic.FILTER queryOptions.shouldApplyDeDuplication()
? ListResultsConsumer.UniqueSemantic.FILTER
: ListResultsConsumer.UniqueSemantic.NONE
); );
} }
finally { finally {
@ -205,13 +207,7 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
// NOTE : if we get here we have a resultType of some kind // NOTE : if we get here we have a resultType of some kind
if ( queryOptions.getTupleTransformer() != null ) { if ( selections.size() > 1 ) {
// 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 ) {
throw new IllegalQueryOperationException( "Query defined multiple selections, return cannot be typed (other that Object[] or Tuple)" ); throw new IllegalQueryOperationException( "Query defined multiple selections, return cannot be typed (other that Object[] or Tuple)" );
} }
else { else {

View File

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

View File

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

View File

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

View File

@ -57,10 +57,12 @@ public class ResultsHelper {
ExecutionContext executionContext, ExecutionContext executionContext,
LockOptions lockOptions, LockOptions lockOptions,
RowTransformer<R> rowTransformer, RowTransformer<R> rowTransformer,
Class<R> transformedResultJavaType,
JdbcValues jdbcValues) { JdbcValues jdbcValues) {
final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory();
final Map<NavigablePath, Initializer> initializerMap = new LinkedHashMap<>(); final Map<NavigablePath, Initializer> initializerMap = new LinkedHashMap<>();
final List<Initializer> initializers = new ArrayList<>(); final List<Initializer> initializers = new ArrayList<>();
final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory();
final List<DomainResultAssembler<?>> assemblers = jdbcValues.getValuesMapping().resolveAssemblers( final List<DomainResultAssembler<?>> assemblers = jdbcValues.getValuesMapping().resolveAssemblers(
new AssemblerCreationState() { new AssemblerCreationState() {
@ -107,7 +109,7 @@ public class ResultsHelper {
logInitializers( initializerMap ); logInitializers( initializerMap );
return new StandardRowReader<>( assemblers, initializers, rowTransformer ); return new StandardRowReader<>( assemblers, initializers, rowTransformer, transformedResultJavaType );
} }
private static void logInitializers(Map<NavigablePath, Initializer> initializerMap) { 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<DomainResultAssembler<?>> resultAssemblers;
private final List<Initializer> initializers; private final List<Initializer> initializers;
private final RowTransformer<T> rowTransformer; private final RowTransformer<T> rowTransformer;
private final Class<T> domainResultJavaType;
private final int assemblerCount; private final int assemblerCount;
public StandardRowReader( public StandardRowReader(
List<DomainResultAssembler<?>> resultAssemblers, List<DomainResultAssembler<?>> resultAssemblers,
List<Initializer> initializers, List<Initializer> initializers,
RowTransformer<T> rowTransformer) { RowTransformer<T> rowTransformer,
Class<T> domainResultJavaType) {
this.resultAssemblers = resultAssemblers; this.resultAssemblers = resultAssemblers;
this.initializers = initializers; this.initializers = initializers;
this.rowTransformer = rowTransformer; this.rowTransformer = rowTransformer;
this.assemblerCount = resultAssemblers.size(); this.assemblerCount = resultAssemblers.size();
this.domainResultJavaType = domainResultJavaType;
logDebugInfo(); logDebugInfo();
} }
@ -61,18 +64,22 @@ public class StandardRowReader<T> implements RowReader<T> {
} }
@Override @Override
@SuppressWarnings("unchecked") public Class<T> getDomainResultResultJavaType() {
public Class<T> getResultJavaType() { return domainResultJavaType;
if ( resultAssemblers.size() == 1 ) {
return (Class<T>) resultAssemblers.get( 0 ).getAssembledJavaType().getJavaTypeClass();
}
return (Class<T>) Object[].class;
} }
@Override @Override
public List<JavaType> getResultJavaTypes() { public Class<?> getResultJavaType() {
List<JavaType> javaTypes = new ArrayList<>( resultAssemblers.size() ); 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 ) { for ( DomainResultAssembler resultAssembler : resultAssemblers ) {
javaTypes.add( resultAssembler.getAssembledJavaType() ); javaTypes.add( resultAssembler.getAssembledJavaType() );
} }

View File

@ -8,50 +8,94 @@ package org.hibernate.sql.results.spi;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.query.ResultListTransformer; 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.JdbcValues;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; 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.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 * @author Steve Ebersole
*/ */
public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> { public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
/** private static final ListResultsConsumer<?> NEVER_DE_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.NEVER );
* Singleton access 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 UNIQUE_FILTER_INSTANCE = new ListResultsConsumer( UniqueSemantic.FILTER ); private static final ListResultsConsumer<?> ERROR_DUP_CONSUMER = new ListResultsConsumer<>( UniqueSemantic.ASSERT );
private static final ListResultsConsumer NORMAL_INSTANCE = new ListResultsConsumer( UniqueSemantic.NONE );
private static final ListResultsConsumer UNIQUE_INSTANCE = new ListResultsConsumer( UniqueSemantic.ASSERT );
@SuppressWarnings("unchecked")
public static <R> ListResultsConsumer<R> instance(UniqueSemantic uniqueSemantic) { public static <R> ListResultsConsumer<R> instance(UniqueSemantic uniqueSemantic) {
switch ( uniqueSemantic ) { switch ( uniqueSemantic ) {
case ASSERT: case ASSERT: {
return UNIQUE_INSTANCE; return (ListResultsConsumer<R>) ERROR_DUP_CONSUMER;
case FILTER: }
return UNIQUE_FILTER_INSTANCE; case FILTER: {
default: return (ListResultsConsumer<R>) DE_DUP_CONSUMER;
return NORMAL_INSTANCE; }
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 { public enum UniqueSemantic {
/**
* Apply no in-memory de-duplication
*/
NONE, NONE,
/**
* Apply in-memory de-duplication, removing rows already part of the results
*/
FILTER, 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 UniqueSemantic uniqueSemantic;
private final ResultHandler<R> resultHandler;
public ListResultsConsumer(UniqueSemantic uniqueSemantic) { public ListResultsConsumer(UniqueSemantic uniqueSemantic) {
this.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 @Override
@ -63,61 +107,75 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
RowProcessingStateStandardImpl rowProcessingState, RowProcessingStateStandardImpl rowProcessingState,
RowReader<R> rowReader) { RowReader<R> rowReader) {
final PersistenceContext persistenceContext = session.getPersistenceContext(); final PersistenceContext persistenceContext = session.getPersistenceContext();
final TypeConfiguration typeConfiguration = session.getTypeConfiguration();
final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry();
final QueryOptions queryOptions = rowProcessingState.getQueryOptions();
RuntimeException ex = null; RuntimeException ex = null;
try { try {
persistenceContext.getLoadContexts().register( jdbcValuesSourceProcessingState ); persistenceContext.getLoadContexts().register( jdbcValuesSourceProcessingState );
final List<R> results = new ArrayList<>(); final List<R> results = new ArrayList<>();
boolean uniqueRows = false; final JavaType<R> domainResultJavaType;
final ResultHandler<R> resultHandlerToUse;
if ( uniqueSemantic != UniqueSemantic.NONE ) { final TupleTransformer<?> tupleTransformer = queryOptions.getTupleTransformer();
final Class<R> resultJavaType = rowReader.getResultJavaType(); if ( tupleTransformer instanceof TypedTupleTransformer ) {
if ( resultJavaType != null && !resultJavaType.isArray() ) { //noinspection unchecked
final EntityPersister entityDescriptor = session.getFactory() final TypedTupleTransformer<R> typedTupleTransformer = (TypedTupleTransformer<R>) tupleTransformer;
.getRuntimeMetamodels() domainResultJavaType = javaTypeRegistry.resolveDescriptor( typedTupleTransformer.getTransformedType() );
.getMappingMetamodel()
.findEntityDescriptor( resultJavaType );
if ( entityDescriptor != null ) {
uniqueRows = true;
}
}
}
if ( uniqueRows ) { if ( uniqueSemantic == UniqueSemantic.NEVER ) {
final List<JavaType> resultJavaTypes = rowReader.getResultJavaTypes(); resultHandlerToUse = this.resultHandler;
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()
);
} }
add = false; else if ( queryOptions.shouldApplyDeDuplication() ) {
break; resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
} }
else if ( domainResultJavaType instanceof EntityJavaType ) {
resultHandlerToUse = ListResultsConsumer::deDuplicationHandling;
} }
if ( add ) { else {
results.add( row ); resultHandlerToUse = this.resultHandler;
}
rowProcessingState.finishRowProcessing();
} }
} }
else { 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() ) { while ( rowProcessingState.next() ) {
results.add( rowReader.readRow( rowProcessingState, processingOptions ) ); final R row = rowReader.readRow( rowProcessingState, processingOptions );
resultHandlerToUse.handle( row, domainResultJavaType, results, rowProcessingState );
rowProcessingState.finishRowProcessing(); rowProcessingState.finishRowProcessing();
} }
}
try { try {
jdbcValuesSourceProcessingState.finishUp(); jdbcValuesSourceProcessingState.finishUp();
} }
@ -126,12 +184,11 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
} }
//noinspection unchecked //noinspection unchecked
final ResultListTransformer<R> resultListTransformer = (ResultListTransformer<R>) jdbcValuesSourceProcessingState.getExecutionContext() final ResultListTransformer<R> resultListTransformer = (ResultListTransformer<R>) queryOptions.getResultListTransformer();
.getQueryOptions()
.getResultListTransformer();
if ( resultListTransformer != null ) { if ( resultListTransformer != null ) {
return resultListTransformer.transformList( results ); return resultListTransformer.transformList( results );
} }
return results; return results;
} }
catch (RuntimeException e) { catch (RuntimeException e) {
@ -160,8 +217,108 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
throw new IllegalStateException( "Should not reach this!" ); 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 @Override
public boolean canResultsBeCached() { public boolean canResultsBeCached() {
return true; 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; 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 * @author Steve Ebersole
*/ */
public interface ResultsConsumer<T, R> { public interface ResultsConsumer<T, R> {

View File

@ -23,27 +23,38 @@ import org.hibernate.type.descriptor.java.JavaType;
*/ */
public interface RowReader<R> { public interface RowReader<R> {
/** /**
* The overall row result Java type. Might be a scalar type, an * The type actually returned from this reader's {@link #readRow} call,
* entity type, etc. Might also be a `Object[].class` for multiple * accounting for any transformers.
* results (domain selections). * <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(); List<Initializer> getInitializers();
/** /**
* The actual coordination of reading a row * 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); R readRow(RowProcessingState processingState, JdbcValuesSourceProcessingOptions options);

View File

@ -9,11 +9,12 @@ package org.hibernate.transform;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import org.hibernate.QueryException; import org.hibernate.QueryException;
import org.hibernate.query.TypedTupleTransformer;
/** /**
* Wraps the tuples in a constructor call. * 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; private final Constructor<T> constructor;
@ -26,6 +27,11 @@ public class AliasToBeanConstructorResultTransformer<T> implements ResultTransfo
this.constructor = constructor; this.constructor = constructor;
} }
@Override
public Class<T> getTransformedType() {
return constructor.getDeclaringClass();
}
/** /**
* Wrap the incoming tuples in a call to our configured constructor. * 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.PropertyAccessStrategyFieldImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyMapImpl; import org.hibernate.property.access.internal.PropertyAccessStrategyMapImpl;
import org.hibernate.property.access.spi.Setter; import org.hibernate.property.access.spi.Setter;
import org.hibernate.query.TypedTupleTransformer;
/** /**
* Result transformer that allows to transform a result to * Result transformer that allows to transform a result to
@ -22,7 +23,7 @@ import org.hibernate.property.access.spi.Setter;
* *
* @author max * @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 // IMPL NOTE : due to the delayed population of setters (setters cached
// for performance), we really cannot properly define equality for // for performance), we really cannot properly define equality for
@ -41,6 +42,11 @@ public class AliasToBeanResultTransformer<T> implements ResultTransformer<T> {
this.resultClass = resultClass; this.resultClass = resultClass;
} }
@Override
public Class<T> getTransformedType() {
return resultClass;
}
@Override @Override
public T transformTuple(Object[] tuple, String[] aliases) { public T transformTuple(Object[] tuple, String[] aliases) {
T result; T result;

View File

@ -8,6 +8,7 @@ package org.hibernate.transform;
import java.util.Map; import java.util.Map;
import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.query.TypedTupleTransformer;
/** /**
* {@link ResultTransformer} implementation which builds a map for each "row", * {@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 Gavin King
* @author Steve Ebersole * @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(); public static final AliasToEntityMapResultTransformer INSTANCE = new AliasToEntityMapResultTransformer();
@ -26,6 +27,12 @@ public class AliasToEntityMapResultTransformer implements ResultTransformer<Map<
private AliasToEntityMapResultTransformer() { private AliasToEntityMapResultTransformer() {
} }
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Class getTransformedType() {
return Map.class;
}
@Override @Override
public Map<String,Object> transformTuple(Object[] tuple, String[] aliases) { public Map<String,Object> transformTuple(Object[] tuple, String[] aliases) {
Map<String,Object> result = CollectionHelper.mapOfSize( tuple.length ); 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.Arrays;
import java.util.List; 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 * 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(); public static final ToListResultTransformer INSTANCE = new ToListResultTransformer();
/** /**
@ -21,6 +23,12 @@ public class ToListResultTransformer implements ResultTransformer<List<Object>>
private ToListResultTransformer() { private ToListResultTransformer() {
} }
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class<List<Object>> getTransformedType() {
return (Class) List.class;
}
@Override @Override
public List<Object> transformTuple(Object[] tuple, String[] aliases) { public List<Object> transformTuple(Object[] tuple, String[] aliases) {
return Arrays.asList( tuple ); 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.Product;
import org.hibernate.testing.orm.domain.retail.Vendor; import org.hibernate.testing.orm.domain.retail.Vendor;
import org.hibernate.testing.orm.junit.DomainModel; 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.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
@DomainModel( standardModels = StandardDomainModel.RETAIL ) @DomainModel( standardModels = StandardDomainModel.RETAIL )
@SessionFactory @SessionFactory
@JiraKey( "HHH-15133" )
public class ImplicitSelectWithJoinTests { public class ImplicitSelectWithJoinTests {
private static final String HQL = "from Product p join p.vendor v where v.name like '%Steve%'"; 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; 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; 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 @Id
public Integer getId() { public Integer getId() {
return id; return id;

View File

@ -25,6 +25,20 @@ public class Order {
private Payment payment; private Payment payment;
private SalesAssociate salesAssociate; 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 @Id
public Integer getId() { public Integer getId() {
return id; 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();
}