From e60f00cec72d0334eb1cc329e4a6206975db67f3 Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 28 May 2023 12:14:44 +0200 Subject: [PATCH 1/9] HHH-16710 implicit instantiation of Lists, Maps --- .../hibernate/NonUniqueResultException.java | 2 +- .../AbstractSharedSessionContract.java | 3 +- .../query/spi/AbstractSelectionQuery.java | 105 ++++++++------ .../org/hibernate/query/spi/SqmQuery.java | 2 +- .../query/sqm/SqmSelectionQuery.java | 6 +- .../internal/ConcreteSqmSelectQueryPlan.java | 25 ++-- .../query/sqm/internal/QuerySqmImpl.java | 10 +- .../sqm/internal/SqmSelectionQueryImpl.java | 110 +++++++------- .../internal/RowTransformerListImpl.java | 32 +++++ .../internal/RowTransformerMapImpl.java | 48 +++++++ .../sql/results/internal/TupleImpl.java | 10 +- .../query/hql/ImplicitInstantiationTest.java | 135 ++++++++++++++++++ 12 files changed, 357 insertions(+), 131 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java b/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java index 435fbdf95dd9..328211429bab 100644 --- a/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java +++ b/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java @@ -23,7 +23,7 @@ public class NonUniqueResultException extends HibernateException { * @param resultCount The number of actual results. */ public NonUniqueResultException(int resultCount) { - super( "query did not return a unique result: " + resultCount ); + super( "Query did not return a unique result: " + resultCount + " results were returned" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index ea28e3594eb5..9bdb1ae701f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -742,7 +742,7 @@ protected static void checkSelectionQuery(String hql, HqlInterpretation hqlInter } } - protected static void checkResultType(Class expectedResultType, SqmSelectionQueryImpl query) { + protected static void checkResultType(Class expectedResultType, SqmSelectionQueryImpl query) { final Class resultType = query.getResultType(); if ( !expectedResultType.isAssignableFrom( resultType ) ) { throw new QueryTypeMismatchException( @@ -849,6 +849,7 @@ protected void addResultType(Class resultClass, NativeQueryImplementor if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( new NativeQueryTupleTransformer() ); } + // TODO: handle Map, List as well else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index ccc2c001c662..54ee284559b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -104,40 +104,56 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) { super( session ); } + public static boolean isTupleResultClass(Class resultType) { + return Tuple.class.isAssignableFrom( resultType ) + || Map.class.isAssignableFrom( resultType ); + } + protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class resultType) { - if ( resultType != null && Tuple.class.isAssignableFrom( resultType ) ) { - final List> selections = ( (SqmSelectStatement) statement ).getQueryPart() - .getFirstQuerySpec() - .getSelectClause() - .getSelections(); - // resultType is Tuple.. + if ( resultType == null ) { + return null; + } + else if ( isTupleResultClass( resultType ) ) { + final List> selections = + ( (SqmSelectStatement) statement ).getQueryPart() + .getFirstQuerySpec() + .getSelectClause() + .getSelections(); if ( getQueryOptions().getTupleTransformer() == null ) { - final Map, Integer> tupleElementMap; - if ( selections.size() == 1 && selections.get( 0 ).getSelectableNode() instanceof CompoundSelection ) { - final List> selectionItems = selections.get( 0 ) - .getSelectableNode() - .getSelectionItems(); - tupleElementMap = new IdentityHashMap<>( selectionItems.size() ); - for ( int i = 0; i < selectionItems.size(); i++ ) { - tupleElementMap.put( selectionItems.get( i ), i ); - } - } - else { - tupleElementMap = new IdentityHashMap<>( selections.size() ); - for ( int i = 0; i < selections.size(); i++ ) { - final SqmSelection selection = selections.get( i ); - tupleElementMap.put( selection.getSelectableNode(), i ); - } - } - return new TupleMetadata( tupleElementMap ); + return new TupleMetadata( buildTupleElementMap( selections ) ); + } + else { + throw new IllegalArgumentException( + "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + + getQueryOptions().getTupleTransformer() + ); } + } + else { + return null; + } + } - throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + - getQueryOptions().getTupleTransformer() - ); + private static Map, Integer> buildTupleElementMap(List> selections) { + final Map, Integer> tupleElementMap; + if ( selections.size() == 1 + && selections.get( 0 ).getSelectableNode() instanceof CompoundSelection ) { + final List> selectionItems = + selections.get( 0 ).getSelectableNode() + .getSelectionItems(); + tupleElementMap = new IdentityHashMap<>( selectionItems.size() ); + for ( int i = 0; i < selectionItems.size(); i++ ) { + tupleElementMap.put( selectionItems.get( i ), i ); + } } - return null; + else { + tupleElementMap = new IdentityHashMap<>( selections.size() ); + for (int i = 0; i < selections.size(); i++ ) { + final SqmSelection selection = selections.get( i ); + tupleElementMap.put( selection.getSelectableNode(), i ); + } + } + return tupleElementMap; } protected void applyOptions(NamedSqmQueryMemento memento) { @@ -207,7 +223,7 @@ protected void applyOptions(NamedQueryMemento memento) { */ protected void visitQueryReturnType( SqmQueryPart queryPart, - Class resultType, + Class expectedResultType, SessionFactoryImplementor factory) { assert getQueryString().equals( CRITERIA_HQL_STRING ); @@ -231,33 +247,35 @@ protected void visitQueryReturnType( } } - if ( resultType != null ) { - checkQueryReturnType( sqmQuerySpec, resultType, factory ); + if ( expectedResultType != null ) { + checkQueryReturnType( sqmQuerySpec, expectedResultType, factory ); } } else { final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; for ( SqmQueryPart sqmQueryPart : queryGroup.getQueryParts() ) { - visitQueryReturnType( sqmQueryPart, resultType, factory ); + visitQueryReturnType( sqmQueryPart, expectedResultType, factory ); } } } protected static void checkQueryReturnType( SqmQuerySpec querySpec, - Class resultClass, + Class expectedResultClass, SessionFactoryImplementor sessionFactory) { - if ( resultClass == null || resultClass == Object.class ) { + if ( expectedResultClass == null || expectedResultClass == Object.class ) { // nothing to check return; } final List> selections = querySpec.getSelectClause().getSelections(); - if ( resultClass.isArray() ) { + if ( expectedResultClass.isArray() ) { // todo (6.0) : implement } - else if ( Tuple.class.isAssignableFrom( resultClass ) ) { + else if ( Tuple.class.isAssignableFrom( expectedResultClass ) + || Map.class.isAssignableFrom( expectedResultClass ) + || List.class.isAssignableFrom( expectedResultClass ) ) { // todo (6.0) : implement } else { @@ -290,21 +308,18 @@ else if ( Tuple.class.isAssignableFrom( resultClass ) ) { if ( jpaQueryComplianceEnabled ) { return; } - verifyResultType( resultClass, sqmSelection.getNodeType(), sessionFactory ); + verifyResultType( expectedResultClass, sqmSelection.getNodeType() ); } } - protected static void verifyResultType( - Class resultClass, - SqmExpressible sqmExpressible, - SessionFactoryImplementor sessionFactory) { + protected static void verifyResultType(Class resultClass, SqmExpressible sqmExpressible) { assert sqmExpressible != null; final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); assert expressibleJavaType != null; final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); if ( !resultClass.isAssignableFrom( javaTypeClass ) ) { if ( expressibleJavaType instanceof PrimitiveJavaType ) { - if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() == resultClass ) { + if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() == resultClass ) { return; } throwQueryTypeMismatchException( resultClass, sqmExpressible ); @@ -887,7 +902,7 @@ public SelectionQuery setParameter(int position, Date value, TemporalType tem } @Override - public SelectionQuery setParameterList(String name, Collection values) { + public SelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( name, values ); return this; } @@ -995,7 +1010,7 @@ public

SelectionQuery setParameterList(QueryParameter

parameter, P[] v } @Override - public SelectionQuery setProperties(Map map) { + public SelectionQuery setProperties(@SuppressWarnings("rawtypes") Map map) { super.setProperties( map ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java index 7796b1514670..4c3a09850d05 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java @@ -149,7 +149,7 @@ public interface SqmQuery extends CommonQueryContract { SqmQuery setProperties(Object bean); @Override - SqmQuery setProperties(Map bean); + SqmQuery setProperties(@SuppressWarnings("rawtypes") Map bean); @Override SqmQuery setHibernateFlushMode(FlushMode flushMode); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java index a4b450f5c5d2..0b5c20144ec2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java @@ -82,7 +82,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery { SqmSelectionQuery setParameter(Parameter param, Date value, TemporalType temporalType); @Override - SqmSelectionQuery setParameterList(String name, Collection values); + SqmSelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values); @Override

SqmSelectionQuery setParameterList(String name, Collection values, Class

javaType); @@ -100,7 +100,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery {

SqmSelectionQuery setParameterList(String name, P[] values, BindableType

type); @Override - SqmSelectionQuery setParameterList(int position, Collection values); + SqmSelectionQuery setParameterList(int position, @SuppressWarnings("rawtypes") Collection values); @Override

SqmSelectionQuery setParameterList(int position, Collection values, Class

javaType); @@ -139,7 +139,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery { SqmSelectionQuery setProperties(Object bean); @Override - SqmSelectionQuery setProperties(Map bean); + SqmSelectionQuery setProperties(@SuppressWarnings("rawtypes") Map bean); @Override SqmSelectionQuery setHibernateFlushMode(FlushMode flushMode); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index 40c86a91cd52..e2cb86d77935 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; +import jakarta.persistence.Tuple; +import org.hibernate.AssertionFailure; import org.hibernate.ScrollMode; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -41,7 +43,6 @@ import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.FromClauseAccess; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -50,6 +51,8 @@ import org.hibernate.sql.results.graph.entity.LoadingEntityEntry; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; import org.hibernate.sql.results.internal.RowTransformerJpaTupleImpl; +import org.hibernate.sql.results.internal.RowTransformerListImpl; +import org.hibernate.sql.results.internal.RowTransformerMapImpl; import org.hibernate.sql.results.internal.RowTransformerSingularReturnImpl; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.internal.RowTransformerTupleTransformerAdapter; @@ -183,22 +186,26 @@ protected static RowTransformer determineRowTransformer( if ( resultType == Object[].class ) { return (RowTransformer) RowTransformerArrayImpl.instance(); } + else if ( List.class.equals( resultType ) ) { + return (RowTransformer) RowTransformerListImpl.instance(); + } // NOTE : if we get here : // 1) there is no TupleTransformer specified // 2) an explicit result-type, other than an array, was specified - final List> selections = sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections(); + final List> selections = + sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections(); if ( tupleMetadata != null ) { - // resultType is Tuple.. - if ( queryOptions.getTupleTransformer() == null ) { + if ( Tuple.class.equals( resultType ) ) { return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); } - - throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + - queryOptions.getTupleTransformer() - ); + else if ( Map.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + } + else { + throw new AssertionFailure( "Wrong result type for tuple handling: " + resultType ); + } } // NOTE : if we get here we have a resultType of some kind diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 6fc3b7189487..38a3ec0811cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -218,7 +218,7 @@ public QuerySqmImpl( */ public QuerySqmImpl( SqmStatement criteria, - Class resultType, + Class expectedResultType, SharedSessionContractImplementor producer) { super( producer ); this.hql = CRITERIA_HQL_STRING; @@ -262,12 +262,12 @@ public QuerySqmImpl( queryPart.validateQueryStructureAndFetchOwners(); visitQueryReturnType( queryPart, - resultType, + expectedResultType, producer.getFactory() ); } else { - if ( resultType != null ) { + if ( expectedResultType != null ) { throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, @@ -288,8 +288,8 @@ else if ( sqm instanceof SqmInsertStatement ) { } } - this.resultType = resultType; - this.tupleMetadata = buildTupleMetadata( criteria, resultType ); + this.resultType = expectedResultType; + this.tupleMetadata = buildTupleMetadata( criteria, expectedResultType ); } private void validateStatement(SqmStatement sqmStatement, Class resultType) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index f91ae9225358..e5d930258200 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -21,7 +21,6 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.Parameter; import jakarta.persistence.TemporalType; -import jakarta.persistence.Tuple; import org.hibernate.CacheMode; import org.hibernate.FlushMode; @@ -55,7 +54,6 @@ import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sqm.SqmSelectionQuery; import org.hibernate.query.sqm.internal.SqmInterpretationsKey.InterpretationsKeySource; -import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; @@ -74,6 +72,8 @@ import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_RETRIEVE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_STORE_MODE; import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions; +import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * @author Steve Ebersole @@ -86,7 +86,8 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery implemen private final DomainParameterXref domainParameterXref; private final QueryParameterBindingsImpl parameterBindings; - private final Class resultType; + private final Class expectedResultType; + private final Class resultType; private final TupleMetadata tupleMetadata; public SqmSelectionQueryImpl( @@ -104,30 +105,35 @@ public SqmSelectionQueryImpl( this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + this.expectedResultType = expectedResultType; // visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); - this.resultType = determineResultType( sqm, expectedResultType ); + this.resultType = determineResultType( sqm ); setComment( hql ); this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } - private static Class determineResultType(SqmSelectStatement sqm, Class expectedResultType) { - if ( expectedResultType != null && expectedResultType.equals( Tuple.class ) ) { - //noinspection unchecked - return (Class) Tuple.class; + private Class determineResultType(SqmSelectStatement sqm) { + if ( expectedResultType != null ) { + if ( expectedResultType.isArray() ) { + return Object[].class; + } + else if ( List.class.isAssignableFrom( expectedResultType ) ) { + return expectedResultType; + } + else if ( isTupleResultClass( expectedResultType ) ) { + return expectedResultType; + } + else { + return Object[].class; + } } - - if ( expectedResultType == null || ! expectedResultType.isArray() ) { + else { final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); - if ( selections.size() == 1 ) { - final SqmSelection sqmSelection = selections.get( 0 ); - //noinspection unchecked - return (Class) sqmSelection.getNodeJavaType().getJavaTypeClass(); - } + return selections.size() == 1 + ? selections.get(0).getNodeJavaType().getJavaTypeClass() + : Object[].class; } - - //noinspection unchecked - return (Class) Object[].class; } public SqmSelectionQueryImpl( @@ -136,6 +142,7 @@ public SqmSelectionQueryImpl( SharedSessionContractImplementor session) { super( session ); this.hql = memento.getHqlString(); + this.expectedResultType = resultType; this.resultType = resultType; final SessionFactoryImplementor factory = session.getFactory(); @@ -164,10 +171,10 @@ public SqmSelectionQueryImpl( public SqmSelectionQueryImpl( NamedCriteriaQueryMementoImpl memento, - Class resultType, + Class expectedResultType, SharedSessionContractImplementor session) { //noinspection unchecked - this( (SqmSelectStatement) memento.getSqmStatement(), resultType, session); + this( (SqmSelectStatement) memento.getSqmStatement(), expectedResultType, session ); applyOptions( memento ); } @@ -177,27 +184,20 @@ public SqmSelectionQueryImpl( SharedSessionContractImplementor session) { super( session ); this.hql = CRITERIA_HQL_STRING; - if ( session.isCriteriaCopyTreeEnabled() ) { - this.sqm = criteria.copy( SqmCopyContext.simpleContext() ); - } - else { - this.sqm = criteria; - } + this.sqm = session.isCriteriaCopyTreeEnabled() ? criteria.copy( simpleContext() ) : criteria; this.domainParameterXref = DomainParameterXref.from( sqm ); - if ( ! domainParameterXref.hasParameters() ) { - this.parameterMetadata = ParameterMetadataImpl.EMPTY; - } - else { - this.parameterMetadata = new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ); - } + this.parameterMetadata = domainParameterXref.hasParameters() + ? new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ) + : ParameterMetadataImpl.EMPTY; this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here - for ( SqmParameter sqmParameter : this.domainParameterXref.getParameterResolutions().getSqmParameters() ) { + for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - final JpaCriteriaParameter jpaCriteriaParameter = ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); + final JpaCriteriaParameter jpaCriteriaParameter = + ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); final Object value = jpaCriteriaParameter.getValue(); // We don't set a null value, unless the type is also null which is the case when using HibernateCriteriaBuilder.value if ( value != null || jpaCriteriaParameter.getNodeType() == null ) { @@ -209,7 +209,8 @@ public SqmSelectionQueryImpl( } } - this.resultType = determineResultType( sqm, expectedResultType ); + this.expectedResultType = expectedResultType; + this.resultType = determineResultType( sqm ); visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); setComment( hql ); @@ -336,12 +337,10 @@ protected ScrollableResultsImplementor doScroll(ScrollMode scrollMode) { // Query plan private SelectQueryPlan resolveQueryPlan() { - final QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey( this ); + final QueryInterpretationCache.Key cacheKey = createInterpretationsKey( this ); if ( cacheKey != null ) { - return getSession().getFactory().getQueryEngine().getInterpretationCache().resolveSelectQueryPlan( - cacheKey, - this::buildQueryPlan - ); + return getSession().getFactory().getQueryEngine().getInterpretationCache() + .resolveSelectQueryPlan( cacheKey, this::buildQueryPlan ); } else { return buildQueryPlan(); @@ -354,40 +353,29 @@ private SelectQueryPlan buildQueryPlan() { getSession().getFactory() ); - if ( concreteSqmStatements.length > 1 ) { - return buildAggregatedQueryPlan( concreteSqmStatements ); - } - else { - return buildConcreteQueryPlan( concreteSqmStatements[0], getResultType(), getQueryOptions() ); - } + return concreteSqmStatements.length > 1 + ? buildAggregatedQueryPlan( concreteSqmStatements ) + : buildConcreteQueryPlan( concreteSqmStatements[0], getQueryOptions() ); } private SelectQueryPlan buildAggregatedQueryPlan(SqmSelectStatement[] concreteSqmStatements) { //noinspection unchecked final SelectQueryPlan[] aggregatedQueryPlans = new SelectQueryPlan[ concreteSqmStatements.length ]; - // todo (6.0) : we want to make sure that certain thing (ResultListTransformer, etc) only get applied at the aggregator-level - for ( int i = 0, x = concreteSqmStatements.length; i < x; i++ ) { - aggregatedQueryPlans[i] = buildConcreteQueryPlan( - concreteSqmStatements[i], - getResultType(), - getQueryOptions() - ); + aggregatedQueryPlans[i] = buildConcreteQueryPlan( concreteSqmStatements[i], getQueryOptions() ); } - return new AggregatedSelectQueryPlanImpl<>( aggregatedQueryPlans ); } - private SelectQueryPlan buildConcreteQueryPlan( + private SelectQueryPlan buildConcreteQueryPlan( SqmSelectStatement concreteSqmStatement, - Class resultType, QueryOptions queryOptions) { return new ConcreteSqmSelectQueryPlan<>( concreteSqmStatement, getQueryString(), getDomainParameterXref(), - resultType, + expectedResultType, tupleMetadata, queryOptions ); @@ -399,7 +387,7 @@ private SelectQueryPlan buildConcreteQueryPlan( // InterpretationsKeySource @Override - public Class getResultType() { + public Class getResultType() { return resultType; } @@ -684,7 +672,7 @@ public SqmSelectionQuery setParameter(int position, Date value, TemporalType } @Override - public SqmSelectionQuery setParameterList(String name, Collection values) { + public SqmSelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( name, values ); return this; } @@ -720,7 +708,7 @@ public

SqmSelectionQuery setParameterList(String name, P[] values, Bindab } @Override - public SqmSelectionQuery setParameterList(int position, Collection values) { + public SqmSelectionQuery setParameterList(int position, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( position, values ); return this; } @@ -792,7 +780,7 @@ public

SqmSelectionQuery setParameterList(QueryParameter

parameter, P[ } @Override - public SqmSelectionQuery setProperties(Map map) { + public SqmSelectionQuery setProperties(@SuppressWarnings("rawtypes") Map map) { super.setProperties( map ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java new file mode 100644 index 000000000000..3ce50e007ace --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java @@ -0,0 +1,32 @@ +/* + * 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.sql.results.internal; + +import org.hibernate.sql.results.spi.RowTransformer; + +import java.util.List; + +/** + * RowTransformer used when an array is explicitly specified as the return type + * + * @author Steve Ebersole + */ +public class RowTransformerListImpl implements RowTransformer> { + /** + * Singleton access + */ + public static final RowTransformerListImpl INSTANCE = new RowTransformerListImpl(); + + public static RowTransformerListImpl instance() { + return INSTANCE; + } + + @Override + public List transformRow(Object[] row) { + return List.of( row ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java new file mode 100644 index 000000000000..aa481c6ab4af --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java @@ -0,0 +1,48 @@ +/* + * 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.sql.results.internal; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * RowTransformer generating a JPA {@link Tuple} + * + * @author Steve Ebersole + */ +public class RowTransformerMapImpl implements RowTransformer> { + private final TupleMetadata tupleMetadata; + + public RowTransformerMapImpl(TupleMetadata tupleMetadata) { + this.tupleMetadata = tupleMetadata; + } + + @Override + public Map transformRow(Object[] row) { + Map map = new HashMap<>( row.length ); + List> list = tupleMetadata.getList(); + for ( int i = 0; i < row.length; i++ ) { + String alias = list.get(i).getAlias(); + if ( alias == null ) { + alias = Integer.toString(i); + } + map.put( alias, row[i] ); + } + return map; + } + + @Override + public int determineNumberOfResultElements(int rawElementCount) { + return 1; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java index 9c5a4f1300d9..7869ef50c9a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java @@ -10,9 +10,10 @@ import java.util.List; import jakarta.persistence.TupleElement; -import org.hibernate.internal.util.type.PrimitiveWrapperHelper; import org.hibernate.query.JpaTuple; +import static org.hibernate.internal.util.type.PrimitiveWrapperHelper.getDescriptorByPrimitiveType; + /** * Implementation of the JPA Tuple contract * @@ -45,7 +46,7 @@ public X get(TupleElement tupleElement) { public X get(String alias, Class type) { final Object untyped = get( alias ); if ( untyped != null ) { - if (!elementTypeMatches(type, untyped)) { + if ( !elementTypeMatches( type, untyped ) ) { throw new IllegalArgumentException( String.format( "Requested tuple value [alias=%s, value=%s] cannot be assigned to requested type [%s]", @@ -99,9 +100,8 @@ public Object get(int i) { } private boolean elementTypeMatches(Class type, Object untyped) { - return type.isInstance(untyped) - || type.isPrimitive() - && PrimitiveWrapperHelper.getDescriptorByPrimitiveType( type).getWrapperClass().isInstance( untyped); + return type.isInstance( untyped ) + || type.isPrimitive() && getDescriptorByPrimitiveType( type ).getWrapperClass().isInstance( untyped ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java new file mode 100644 index 000000000000..6d4cfad42002 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java @@ -0,0 +1,135 @@ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + ImplicitInstantiationTest.Thing.class + } +) +@SessionFactory +public class ImplicitInstantiationTest { + + @Test + public void testTupleInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Tuple result = session.createQuery("select id as id, upper(name) as name from Thing", Tuple.class).getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testTupleInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Tuple result = session.createSelectionQuery("select id, upper(name) from Thing", Tuple.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testMapInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Map result = session.createSelectionQuery("select id, upper(name) from Thing", Map.class).getSingleResult(); + assertEquals( result.get("0"), 1L ); + assertEquals( result.get("1"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testMapInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Map result = session.createQuery("select id as id, upper(name) as name from Thing", Map.class).getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testListInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + List result = session.createSelectionQuery("select id, upper(name) from Thing", List.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testListInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + List result = session.createQuery("select id as id, upper(name) as name from Thing", List.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Entity(name = "Thing") + @Table(name = "thingy_table") + public class Thing { + private Long id; + + private String name; + + public Thing(Long id, String name) { + this.id = id; + this.name = name; + } + + Thing() { + } + + @Id + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} From 4803f730fc5f94217fd5b1c01976f329bbd9f0d7 Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 28 May 2023 17:38:02 +0200 Subject: [PATCH 2/9] HHH-16710 implicit instantiation of record classes --- .../AbstractSharedSessionContract.java | 2 +- .../query/spi/AbstractSelectionQuery.java | 202 ++++++++++-------- .../internal/ConcreteSqmSelectQueryPlan.java | 52 +++-- .../sqm/internal/SqmSelectionQueryImpl.java | 37 ++-- .../RowTransformerConstructorImpl.java | 58 +++++ .../internal/RowTransformerListImpl.java | 4 +- .../internal/RowTransformerMapImpl.java | 5 +- ...RowTransformerTupleTransformerAdapter.java | 5 - .../query/hql/ImplicitInstantiationTest.java | 28 +++ 9 files changed, 249 insertions(+), 144 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 9bdb1ae701f8..60299bceca61 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -748,7 +748,7 @@ protected static void checkResultType(Class expectedResultType, SqmSelect throw new QueryTypeMismatchException( String.format( Locale.ROOT, - "Query result-type error - expecting `%s`, but found `%s`", + "Incorrect query result type: query produces '%s' but type '%s' was given", expectedResultType.getName(), resultType.getName() ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index 54ee284559b0..8dffca663785 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -66,6 +66,7 @@ import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; @@ -104,33 +105,36 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) { super( session ); } - public static boolean isTupleResultClass(Class resultType) { - return Tuple.class.isAssignableFrom( resultType ) - || Map.class.isAssignableFrom( resultType ); - } - protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class resultType) { - if ( resultType == null ) { + if ( isInstantiableWithoutMetadata( resultType ) ) { + // no need to build metadata for instantiating tuples return null; } - else if ( isTupleResultClass( resultType ) ) { + else { + final SqmSelectStatement select = (SqmSelectStatement) statement; final List> selections = - ( (SqmSelectStatement) statement ).getQueryPart() - .getFirstQuerySpec() - .getSelectClause() + select.getQueryPart().getFirstQuerySpec().getSelectClause() .getSelections(); - if ( getQueryOptions().getTupleTransformer() == null ) { - return new TupleMetadata( buildTupleElementMap( selections ) ); + if ( Tuple.class.equals( resultType ) || selections.size() > 1 ) { + return getTupleMetadata( selections ); } else { - throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + - getQueryOptions().getTupleTransformer() - ); + // only one element in select list, + // we don't support instantiation + return null; } } + } + + private TupleMetadata getTupleMetadata(List> selections) { + if ( getQueryOptions().getTupleTransformer() == null ) { + return new TupleMetadata( buildTupleElementMap( selections ) ); + } else { - return null; + throw new IllegalArgumentException( + "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + + getQueryOptions().getTupleTransformer() + ); } } @@ -263,55 +267,52 @@ protected static void checkQueryReturnType( SqmQuerySpec querySpec, Class expectedResultClass, SessionFactoryImplementor sessionFactory) { - if ( expectedResultClass == null || expectedResultClass == Object.class ) { - // nothing to check - return; - } - - final List> selections = querySpec.getSelectClause().getSelections(); - - if ( expectedResultClass.isArray() ) { - // todo (6.0) : implement - } - else if ( Tuple.class.isAssignableFrom( expectedResultClass ) - || Map.class.isAssignableFrom( expectedResultClass ) - || List.class.isAssignableFrom( expectedResultClass ) ) { - // todo (6.0) : implement - } - else { - final boolean jpaQueryComplianceEnabled = sessionFactory.getSessionFactoryOptions() - .getJpaCompliance() - .isJpaQueryComplianceEnabled(); - if ( selections.size() != 1 ) { - final String errorMessage = "Query result-type error - multiple selections: use Tuple or array"; - - if ( jpaQueryComplianceEnabled ) { - throw new IllegalArgumentException( errorMessage ); - } - else { - throw new QueryTypeMismatchException( errorMessage ); + if ( !isResultTypeAlwaysAllowed( expectedResultClass ) ) { + final List> selections = querySpec.getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + // we have one item in the select list, + // the type has to match (no instantiation) + final SqmSelection sqmSelection = selections.get(0); + + // special case for parameters in the select list + final SqmSelectableNode selection = sqmSelection.getSelectableNode(); + if ( selection instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection; + final SqmExpressible nodeType = sqmParameter.getNodeType(); + // we may not yet know a selection type + if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { + // we can't verify the result type up front + return; + } } - } - - final SqmSelection sqmSelection = selections.get( 0 ); - - if ( sqmSelection.getSelectableNode() instanceof SqmParameter ) { - final SqmParameter sqmParameter = (SqmParameter) sqmSelection.getSelectableNode(); - // we may not yet know a selection type - if ( sqmParameter.getNodeType() == null || sqmParameter.getNodeType().getExpressibleJavaType() == null ) { - // we can't verify the result type up front - return; + final boolean jpaQueryComplianceEnabled = + sessionFactory.getSessionFactoryOptions() + .getJpaCompliance() + .isJpaQueryComplianceEnabled(); + if ( !jpaQueryComplianceEnabled ) { + verifyResultType( expectedResultClass, sqmSelection.getNodeType() ); } } - - if ( jpaQueryComplianceEnabled ) { - return; - } - verifyResultType( expectedResultClass, sqmSelection.getNodeType() ); + // else, let's assume we can instantiate it! } } + private static boolean isInstantiableWithoutMetadata(Class resultType) { + return resultType == null + || resultType.isArray() + || Object.class == resultType + || List.class == resultType; + } + + private static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { + return expectedResultClass == null + || expectedResultClass == Object.class + || expectedResultClass == List.class + || expectedResultClass == Tuple.class + || expectedResultClass.isArray(); + } + protected static void verifyResultType(Class resultClass, SqmExpressible sqmExpressible) { assert sqmExpressible != null; final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); @@ -319,46 +320,63 @@ protected static void verifyResultType(Class resultClass, SqmExpressible< final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); if ( !resultClass.isAssignableFrom( javaTypeClass ) ) { if ( expressibleJavaType instanceof PrimitiveJavaType ) { - if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() == resultClass ) { - return; + if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() != resultClass ) { + throwQueryTypeMismatchException( resultClass, sqmExpressible ); } + } + else if ( isMatchingDateType( javaTypeClass, resultClass, sqmExpressible ) ) { + // special case, we are good + } + else { throwQueryTypeMismatchException( resultClass, sqmExpressible ); } - // Special case for date because we always report java.util.Date as expression type - // But the expected resultClass could be a subtype of that, so we need to check the JdbcType - if ( javaTypeClass == Date.class ) { - JdbcType jdbcType = null; - if ( sqmExpressible instanceof BasicDomainType ) { - jdbcType = ( (BasicDomainType) sqmExpressible).getJdbcType(); - } - else if ( sqmExpressible instanceof SqmPathSource ) { - final DomainType domainType = ( (SqmPathSource) sqmExpressible).getSqmPathType(); - if ( domainType instanceof BasicDomainType ) { - jdbcType = ( (BasicDomainType) domainType ).getJdbcType(); + } + } + + // Special case for date because we always report java.util.Date as expression type + // But the expected resultClass could be a subtype of that, so we need to check the JdbcType + private static boolean isMatchingDateType( + Class javaTypeClass, + Class resultClass, + SqmExpressible sqmExpressible) { + return javaTypeClass == Date.class + && isMatchingDateJdbcType( resultClass, getJdbcType( sqmExpressible ) ); + } + + private static JdbcType getJdbcType(SqmExpressible sqmExpressible) { + if ( sqmExpressible instanceof BasicDomainType ) { + return ( (BasicDomainType) sqmExpressible).getJdbcType(); + } + else if ( sqmExpressible instanceof SqmPathSource ) { + final DomainType domainType = ( (SqmPathSource) sqmExpressible).getSqmPathType(); + if ( domainType instanceof BasicDomainType ) { + return ( (BasicDomainType) domainType ).getJdbcType(); + } + } + return null; + } + + private static boolean isMatchingDateJdbcType(Class resultClass, JdbcType jdbcType) { + if ( jdbcType != null ) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { + case Types.DATE: + if ( resultClass.isAssignableFrom( java.sql.Date.class ) ) { + return true; } - } - if ( jdbcType != null ) { - switch ( jdbcType.getDefaultSqlTypeCode() ) { - case Types.DATE: - if ( resultClass.isAssignableFrom( java.sql.Date.class ) ) { - return; - } - break; - case Types.TIME: - if ( resultClass.isAssignableFrom( java.sql.Time.class ) ) { - return; - } - break; - case Types.TIMESTAMP: - if ( resultClass.isAssignableFrom( java.sql.Timestamp.class ) ) { - return; - } - break; + break; + case Types.TIME: + if ( resultClass.isAssignableFrom( java.sql.Time.class ) ) { + return true; } - } + break; + case Types.TIMESTAMP: + if ( resultClass.isAssignableFrom( java.sql.Timestamp.class ) ) { + return true; + } + break; } - throwQueryTypeMismatchException( resultClass, sqmExpressible ); } + return false; } private static void throwQueryTypeMismatchException(Class resultClass, SqmExpressible sqmExpressible) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index e2cb86d77935..b0bcf9ba2ef0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -23,7 +23,6 @@ import org.hibernate.internal.EmptyScrollableResults; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.Query; import org.hibernate.query.TupleTransformer; import org.hibernate.query.spi.DomainQueryExecutionContext; @@ -50,6 +49,7 @@ import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.graph.entity.LoadingEntityEntry; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; +import org.hibernate.sql.results.internal.RowTransformerConstructorImpl; import org.hibernate.sql.results.internal.RowTransformerJpaTupleImpl; import org.hibernate.sql.results.internal.RowTransformerListImpl; import org.hibernate.sql.results.internal.RowTransformerMapImpl; @@ -178,44 +178,40 @@ protected static RowTransformer determineRowTransformer( if ( queryOptions.getTupleTransformer() != null ) { return makeRowTransformerTupleTransformerAdapter( sqm, queryOptions ); } - - if ( resultType == null ) { + else if ( resultType == null ) { return RowTransformerStandardImpl.instance(); } - - if ( resultType == Object[].class ) { + else if ( resultType == Object[].class ) { return (RowTransformer) RowTransformerArrayImpl.instance(); } - else if ( List.class.equals( resultType ) ) { + else if ( resultType == List.class ) { return (RowTransformer) RowTransformerListImpl.instance(); } + else { + // NOTE : if we get here : + // 1) there is no TupleTransformer specified + // 2) an explicit result-type, other than an array, was specified - // NOTE : if we get here : - // 1) there is no TupleTransformer specified - // 2) an explicit result-type, other than an array, was specified - - final List> selections = - sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections(); - if ( tupleMetadata != null ) { - if ( Tuple.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); - } - else if ( Map.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + if ( tupleMetadata == null ) { + if ( sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections().size() == 1 ) { + return RowTransformerSingularReturnImpl.instance(); + } + else { + throw new AssertionFailure( "Query defined multiple selections, should have had TupleMetadata" ); + } } else { - throw new AssertionFailure( "Wrong result type for tuple handling: " + resultType ); + if ( Tuple.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); + } + else if ( Map.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + } + else { + return new RowTransformerConstructorImpl<>( resultType, tupleMetadata ); + } } } - - // NOTE : if we get here we have a resultType of some kind - - if ( selections.size() > 1 ) { - throw new IllegalQueryOperationException( "Query defined multiple selections, return cannot be typed (other that Object[] or Tuple)" ); - } - else { - return RowTransformerSingularReturnImpl.instance(); - } } private static RowTransformer makeRowTransformerTupleTransformerAdapter( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index e5d930258200..9082106b3d87 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -60,6 +60,7 @@ import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.results.internal.TupleMetadata; +import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE; @@ -114,25 +115,35 @@ public SqmSelectionQueryImpl( } private Class determineResultType(SqmSelectStatement sqm) { - if ( expectedResultType != null ) { - if ( expectedResultType.isArray() ) { + final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + if ( Object[].class.equals( expectedResultType ) ) { + // for JPA compatibility return Object[].class; } - else if ( List.class.isAssignableFrom( expectedResultType ) ) { - return expectedResultType; - } - else if ( isTupleResultClass( expectedResultType ) ) { - return expectedResultType; - } else { - return Object[].class; + final SqmSelection selection = selections.get(0); + if ( selection!=null ) { + JavaType javaType = selection.getNodeJavaType(); + if ( javaType != null) { + return javaType.getJavaTypeClass(); + } + } + // due to some error in the query, + // we don't have any information, + // so just let it through so the + // user sees the real error + return expectedResultType; } } + else if ( expectedResultType != null ) { + // assume we can repackage the tuple as + // the given type (worry about how later) + return expectedResultType; + } else { - final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); - return selections.size() == 1 - ? selections.get(0).getNodeJavaType().getJavaTypeClass() - : Object[].class; + // for JPA compatibility + return Object[].class; } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java new file mode 100644 index 000000000000..00b8d30b4218 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java @@ -0,0 +1,58 @@ +/* + * 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.sql.results.internal; + +import jakarta.persistence.TupleElement; +import org.hibernate.InstantiationException; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * {@link RowTransformer} instantiating an arbitrary class + * + * @author Gavin King + */ +public class RowTransformerConstructorImpl implements RowTransformer { + private final Class type; + private final TupleMetadata tupleMetadata; + private final Constructor constructor; + + public RowTransformerConstructorImpl(Class type, TupleMetadata tupleMetadata) { + this.type = type; + this.tupleMetadata = tupleMetadata; + final List> elements = tupleMetadata.getList(); + final Class[] sig = new Class[elements.size()]; + for (int i = 0; i < elements.size(); i++) { + sig[i] = elements.get(i).getJavaType(); + } + try { + constructor = type.getDeclaredConstructor( sig ); + constructor.setAccessible( true ); + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type ", type, e ); + } + } + + @Override + public T transformRow(Object[] row) { + try { + return constructor.newInstance( row ); + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type", type, e ); + } + } + + @Override + public int determineNumberOfResultElements(int rawElementCount) { + return 1; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java index 3ce50e007ace..98b09664a8b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java @@ -11,9 +11,9 @@ import java.util.List; /** - * RowTransformer used when an array is explicitly specified as the return type + * {@link RowTransformer} instantiating a {@link List} * - * @author Steve Ebersole + * @author Gavin King */ public class RowTransformerListImpl implements RowTransformer> { /** diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java index aa481c6ab4af..316b2050aec4 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java @@ -7,7 +7,6 @@ package org.hibernate.sql.results.internal; -import jakarta.persistence.Tuple; import jakarta.persistence.TupleElement; import org.hibernate.sql.results.spi.RowTransformer; @@ -16,9 +15,9 @@ import java.util.Map; /** - * RowTransformer generating a JPA {@link Tuple} + * {@link RowTransformer} instantiating a {@link Map} * - * @author Steve Ebersole + * @author Gavin King */ public class RowTransformerMapImpl implements RowTransformer> { private final TupleMetadata tupleMetadata; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerTupleTransformerAdapter.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerTupleTransformerAdapter.java index 2f9f56cd375e..a14b7fe8f958 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerTupleTransformerAdapter.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerTupleTransformerAdapter.java @@ -29,9 +29,4 @@ public T transformRow(Object[] row) { assert aliases == null || row.length == aliases.length; return tupleTransformer.transformTuple( row, aliases ); } - - @Override - public int determineNumberOfResultElements(int rawElementCount) { - return rawElementCount; - } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java index 6d4cfad42002..8e09b9442ecb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java @@ -22,6 +22,34 @@ @SessionFactory public class ImplicitInstantiationTest { + static class Record { + Long id; + String name; + public Record(Long id, String name) { + this.id = id; + this.name = name; + } + Long id() { + return id; + } + String name() { + return name; + } + } + + @Test + public void testRecordInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Record result = session.createSelectionQuery("select id, upper(name) from Thing", Record.class).getSingleResult(); + assertEquals( result.id(), 1L ); + assertEquals( result.name(), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + @Test public void testTupleInstantiationWithAlias(SessionFactoryScope scope) { scope.inTransaction( From 50f2b7313205669650a7d569bae750589d7971f9 Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 28 May 2023 18:46:36 +0200 Subject: [PATCH 3/9] update docs with implicit instantiation --- .../asciidoc/introduction/Interacting.adoc | 5 +- .../asciidoc/querylanguage/Relational.adoc | 99 +++++++++++++------ 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/documentation/src/main/asciidoc/introduction/Interacting.adoc b/documentation/src/main/asciidoc/introduction/Interacting.adoc index 175b0d71947b..741e1678495f 100644 --- a/documentation/src/main/asciidoc/introduction/Interacting.adoc +++ b/documentation/src/main/asciidoc/introduction/Interacting.adoc @@ -873,7 +873,7 @@ Java's `record` types now offer an interesting alternative: record IsbnTitle(String isbn, String title) {} var results = - session.createSelectionQuery("select new IsbnTitle(isbn, title) from Book", IsbnTitle.class) + session.createSelectionQuery("select isbn, title from Book", IsbnTitle.class) .getResultList(); for (var result : results) { @@ -886,8 +886,7 @@ Notice that we're able to declare the `record` right before the line which execu Now, this is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better. But perhaps you find it more aesthetically pleasing. - -On the other hand, when we're passing query results around the system, the use of `select new` with a `record` type is much better than manually unpacking an `Object[]` array. +And if we're going to be passing query results around the system, the use of a `record` type is _much_ better. Now, the criteria query API offers a much more satisfying solution to the problem. Consider the following code: diff --git a/documentation/src/main/asciidoc/querylanguage/Relational.adoc b/documentation/src/main/asciidoc/querylanguage/Relational.adoc index d4144c697741..ba6520d3a542 100644 --- a/documentation/src/main/asciidoc/querylanguage/Relational.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Relational.adoc @@ -140,6 +140,9 @@ But it's better to specify the projection explicitly, except in the simplest cas There might be multiple items in a projection list, in which case each query result is a tuple, and this poses a problem: Java doesn't have a good way to represent tuples. +[[query-result-types]] +==== Default query result types + If there's just one projected item in the `select` list, then, no sweat, that's the type of each query result. There's no need to bother with trying to represent a "tuple of length 1". @@ -151,7 +154,7 @@ List results = .getResultList(); ---- -But if there are multiple expressions in the select list then, by default, each query result is packaged as an array of type `Object[]`. +But if there are multiple expressions in the select list then, by default, and in compliance with JPA, each query result is packaged as an array of type `Object[]`. [[select-clause-projection-example]] [source, java] @@ -167,12 +170,17 @@ for (var result : results) { } ---- -Or, if explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `javax.persistence.Tuple`. +This is bearable, but let's explore some other options. + +[[query-alt-result-types]] +==== Alternative generic result types + +If explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `javax.persistence.Tuple`, again, as specified by JPA. [source, java] [%unbreakable] ---- -List results = +List tuples = entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", Tuple.class) .getResultList(); @@ -185,9 +193,44 @@ for (Tuple tuple : tuples) { The names of the `Tuple` elements are determined by the aliases given to the projected items in the select list. If no aliases are specified, the elements may be accessed by their position in the list, where the first item is assigned the position zero. -Unfortunately, neither `Object[]` nor `Tuple` lets us access an individual item in a result tuple of an HQL query without explicitly specifying the type of the item, either using a typecast in the case of `Object[]`, or by passing the class object to `get()` in the case of `Tuple`. +As an extension to JPA, Hibernate lets us pass `Map` or `List` here: + +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", + Map.class) + .getResultList(); +for (var map : results) { + String title = (String) tuple.get("title"); + String preamble = (String) tuple.get("preamble"); +} +---- +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + List.class) + .getResultList(); +for (var list : results) { + String title = (String) list.get(0); + String preamble = (String) tuple.get(1); +} +---- + +Unfortunately, none of the types `Object[]`, `List`, `Map`, nor `Tuple` lets us access an individual item in a result tuple without explicitly specifying the type of the item, either using a typecast in the case of `Object[]`, `List`, or `Map`, or by passing the class object to `get()` in the case of `Tuple`. But there's another option, as we're about to see. +[NOTE] +==== +Actually, `Tuple` really exists to service the criteria query API, and in that context it _does_ enable truly typesafe access to query results. +==== + +[[select-new]] +==== Instantiation + Simplifying slightly, the BNF for a projected item is: [[select-item-bnf]] @@ -208,10 +251,7 @@ Where the list of ``selection``s in an `instantiation` is essentially a nested p So there's a special expression type that's only legal in the select clause: the `instantiation` rule in the BNF above. // Let's see what it does. -[[select-new]] -==== Instantiation - -The `select new` construct packages the query results into a user-written Java class instead of an array. +This JPA-standard `select new` construct packages the query results into a user-written Java class instead of an array. [[select-clause-dynamic-instantiation-example]] [source, java] @@ -238,40 +278,35 @@ This class does not need to be mapped or annotated in any way. Even if the class _is_ an entity class, the resulting instances are _not_ managed entities and are _not_ associated with the session. ==== -Alternatively, using the syntax `select new map`, the query may specify that each result should be packaged as a map: +But Hibernate 6 goes one better and makes the `select new` syntax optional. -[[select-clause-dynamic-map-instantiation-example]] +[[select-clause-implicit-instantiation-example]] [source, java] [%unbreakable] ---- -List results = - entityManager.createQuery("select new map(title as title, left(book.text, 200) as summary) from Book", - Map.class) +record BookSummary(String title, String summary) {} + +List results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + BookSummary.class) .getResultList(); +for (var result : results) { + String title = result.title(); + String preamble = result.summary(); +} ---- -The keys of the map are determined by the aliases given to the projected items in the select list. -If no aliases are specified, the key of an item is its position in the list, where the first item is assigned the position zero. +In the past, this functionality required more ceremony. -Or, using the syntax `select new list`, the query may specify that each result should be packaged as a list: +[cols="25,~,~,^15"] +|=== +| Result type | Legacy syntax | Streamlined syntax | JPA standard -[[select-clause-dynamic-list-instantiation-example]] -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select new list(title as title, left(book.text, 200) as summary) from Book", - List.class) - .getResultList(); ----- +| `Map` | `select new map(x, y)` | `select x, y` | ✖/✖ +| `List` | `select new list(x, y)` | `select x, y` | ✖/✖ +| Arbitrary class `Record` | `select new Record(x, y)` | `select x, y` | ✔/✖ +|=== -[NOTE] -==== -This is an older syntax, that predates JPQL. -In hindsight, it's hard to see what advantage `List` offers compared to `Object[]`. -We mention it here only for completeness. -On the other hand, `Map` is a perfectly fine alternative `Tuple`, but of course it isn't portable to other implementations of JPA. -==== [[distinct]] ==== Duplicate removal From 2e78325712f2a6f1fd76f0eb50ff7a9d2adb3de1 Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 29 May 2023 00:57:52 +0200 Subject: [PATCH 4/9] move some material to 1st chapter of HQL guide --- .../main/asciidoc/querylanguage/Concepts.adoc | 220 +++++++++++++++++- .../asciidoc/querylanguage/Relational.adoc | 171 -------------- 2 files changed, 211 insertions(+), 180 deletions(-) diff --git a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc index 8e5521fa9d21..703d2fbaec62 100644 --- a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc @@ -1,13 +1,13 @@ [[basic-concepts]] == Basic concepts -This document describes _Hibernate Query Language_ (HQL), which is, I suppose we could say, a dialect of the _Java_ (now _Jakarta_) _Persistence Query Language_ (JPQL). +This document describes Hibernate Query Language (HQL), which is, I suppose we could say, a dialect of the Java (now Jakarta) Persistence Query Language (JPQL). Or is it the other way around? [NOTE] ==== -JPQL was inspired by early versions of HQL, and is a subset of modern HQL. +JPQL was inspired by early versions of HQL, and is a proper subset of modern HQL. Here we focus on describing the complete, more powerful HQL language as it exists today. If strict JPA compliance is what you're looking for, use the setting `hibernate.jpa.compliance.query=true`. @@ -117,7 +117,15 @@ The JPQL specification defines identification variables as case-_insensitive_. And so in strict JPA-compliant mode, Hibernate treats `person.nickName`, `Person.nickName`, and `PERSON.nickName` as the _same_. ==== -A _quoted identifier_ is written in backticks. Quoting lets you use a keyword as an identifier, for example `` thing.\`select` ``. +A _quoted identifier_ is written in backticks. Quoting lets you use a keyword as an identifier. + +[source,hql] +---- +select thing.interval.`from` from Thing thing +---- + +Actually, in most contexts, HQL keywords are "soft", and don't need to be quoted. +The parser is usually able to distinguish if the reserved word is being used as a keyword or as an identifier. [[comments]] ==== Comments @@ -183,13 +191,14 @@ Since the language must be executed on SQL databases, every type accommodates nu ==== Null values and ternary logic The SQL `null` behaves quite differently to a null value in Java. -In Java, an expression like `number + 1` produces in an exception if `number` is null. -But in SQL, and therefore also in HQL and JPQL, such an expression evaluates to `null`. + +- In Java, an expression like `number + 1` produces in an exception if `number` is null. +- But in SQL, and therefore also in HQL and JPQL, such an expression evaluates to `null`. [IMPORTANT] ==== It's almost always the case that an operation applied to a null value yields another null value. -This applies to function application, to operators like `*` and `||`, to comparison operators like `<` and `=`, and even to logical operations like `and` and `not`. +This rule applies to function application, to operators like `*` and `||`, to comparison operators like `<` and `=`, and even to logical operations like `and` and `not`. The exceptions to this rule are the `is null` operator and the functions `coalesce()` and `ifnull()` which are specifically designed for <>. ==== @@ -344,7 +353,7 @@ The second form may insert many new rows, or none at all. The first sort of `insert` statement is not as useful. It's usually better to just use `persist()`. -On the other hand, you might consider using it to set up test data. +But you might consider using it to set up test data. ==== [NOTE] @@ -527,7 +536,7 @@ from Book book select book.title, book.isbn This form of the query is more readable, because the alias is declared _before_ it's used, just as God and nature intended. ==== -Of course, queries are always polymorphic. +Naturally, queries are always polymorphic. Indeed, a fairly innocent-looking HQL query can easily translate to a SQL statement with many joins and unions. [TIP] @@ -535,4 +544,197 @@ Indeed, a fairly innocent-looking HQL query can easily translate to a SQL statem We need to be a _bit_ careful about that, but actually it's usually a good thing. HQL makes it very easy to fetch all the data we need in a single trip to the database, and that's absolutely key to achieving high performance in data access code. Typically, it's much worse to fetch exactly the data we need, but in many round trips to the database server, than it is to fetch just a bit more data than what we're going to need, all a single SQL query. -==== \ No newline at end of file +==== + +[[returning-to-java]] +=== Representing result sets in Java + +One of the most uncomfortable aspects of working with data in Java is that there's no good way to represent a table. +Languages designed for working with data—R is an excellent example—always feature some sort of built-in table or "data frame" type. +Of course, Java's type system gets in the way here. +This problem is much easier to solve in a dynamically-typed language. +The fundamental problem for Java is that it doesn't have tuple types. + +Queries in Hibernate return tables. +Sure, often a column holds whole entity objects, but we're not restricted to returning a single entity, and we often write queries that return multiple entities in each result, or which return things which aren't entities. + +So we're faced with the problem if representing such result sets, and, we're sad to say, there's no fully general and completely satisfying solution. + +Let's begin with the easy case. + +[[query-result-types-single]] +==== Queries with a single projected item + +If there's just one projected item in the `select` list, then, no sweat, that's the type of each query result. + +[source, java] +[%unbreakable] +---- +List results = + entityManager.createQuery("select title from Book", String.class) + .getResultList(); +---- + +There's really no need to fuss about with trying to represent a "tuple of length 1". +We're not even sure what to call those. + +Problems arise as soon as we have multiple items in the `select` list of a query. + +[[query-result-types-multiple]] +==== Queries with multiple projected items + +When there are multiple expressions in the select list then, by default, and in compliance with JPA, each query result is packaged as an array of type `Object[]`. + +[[select-clause-projection-example]] +[source, java] +[%unbreakable] +---- +List results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + Object[].class) + .getResultList(); +for (var result : results) { + String title = (String) result[0]; + String preamble = (String) result[1]; +} +---- + +This is bearable, but let's explore some other options. + +JPA lets us specify that we want each query result packaged as an instance of `javax.persistence.Tuple`. +All we have to do is pass the class `Tuple` to `createQuery()`. + +[source, java] +[%unbreakable] +---- +List tuples = + entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", + Tuple.class) + .getResultList(); +for (Tuple tuple : tuples) { + String title = tuple.get("title", String.class); + String preamble = tuple.get("preamble", String.class); +} +---- + +The names of the `Tuple` elements are determined by the aliases given to the projected items in the select list. +If no aliases are specified, the elements may be accessed by their position in the list, where the first item is assigned the position zero. + +As an extension to JPA, and in a similar vein, Hibernate lets us pass `Map` or `List`, and have each result packaged as a map or list: + +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", + Map.class) + .getResultList(); +for (var map : results) { + String title = (String) tuple.get("title"); + String preamble = (String) tuple.get("preamble"); +} +---- +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + List.class) + .getResultList(); +for (var list : results) { + String title = (String) list.get(0); + String preamble = (String) tuple.get(1); +} +---- + +Unfortunately, not one of the types `Object[]`, `List`, `Map`, nor `Tuple` lets us access an individual item in a result tuple without a type cast. +Sure `Tuple` does the type cast for us when we pass a class object to `get()`, but it's logically identical. +Fortunately there's one more option, as we're about to see. + +[NOTE] +==== +Actually, `Tuple` really exists to service the criteria query API, and in that context it _does_ enable truly typesafe access to query results. +==== + +Hibernate 6 lets us pass an arbitrary class type with an appropriate constructor to `createQuery()` and will use it to package the query results. +This works extremely nicely with `record` types. + +[[select-clause-implicit-instantiation-example]] +[source, java] +[%unbreakable] +---- +record BookSummary(String title, String summary) {} + +List results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + BookSummary.class) + .getResultList(); +for (var result : results) { + String title = result.title(); + String preamble = result.summary(); +} +---- + +It's important that the constructor of `BookSummary` has parameters which exactly match the items in the `select` list. + +[NOTE] +==== +This class does not need to be mapped or annotated in any way. + +Even if the class _is_ an entity class, the resulting instances are _not_ managed entities and are _not_ associated with the session. +==== + +We must caution that this still isn't typesafe. +In fact, we've just pushed the typecasts down into the call to `createQuery()`. +But at least we don't have to write them explicitly. + +[[select-new]] +==== Instantiation + +In JPA, and in older versions of Hibernate, this functionality required more ceremony. + +[cols="25,~,~,^15"] +|=== +| Result type | Legacy syntax | Streamlined syntax | JPA standard + +| `Map` | `select new map(x, y)` | `select x, y` | ✖/✖ +| `List` | `select new list(x, y)` | `select x, y` | ✖/✖ +| Arbitrary class `Record` | `select new Record(x, y)` | `select x, y` | ✔/✖ +|=== + +For example, the JPA-standard `select new` construct packages the query results into a user-written Java class instead of an array. + +[[select-clause-dynamic-instantiation-example]] +[source, java] +[%unbreakable] +---- +record BookSummary(String title, String summary) {} + +List results = + entityManager.createQuery("select new BookSummary(title, left(book.text, 200)) from Book", + BookSummary.class) + .getResultList(); +for (var result : results) { + String title = result.title(); + String preamble = result.summary(); +} +---- + +Simplifying slightly, the BNF for a projected item is: + +[[select-item-bnf]] +[source, antlrv4] +---- +selection + : (expression | instantiation) alias? + +instantiation + : "NEW" instantiationTarget "(" selection ("," selection)* ")" + +alias + : "AS"? identifier +---- + +Where the list of ``selection``s in an `instantiation` is essentially a nested projection list. + + diff --git a/documentation/src/main/asciidoc/querylanguage/Relational.adoc b/documentation/src/main/asciidoc/querylanguage/Relational.adoc index ba6520d3a542..5d9cc085796e 100644 --- a/documentation/src/main/asciidoc/querylanguage/Relational.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Relational.adoc @@ -137,177 +137,6 @@ If a query has no explicit `select` list, then, as we saw < results = - entityManager.createQuery("select title from Book", String.class) - .getResultList(); ----- - -But if there are multiple expressions in the select list then, by default, and in compliance with JPA, each query result is packaged as an array of type `Object[]`. - -[[select-clause-projection-example]] -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select title, left(book.text, 200) from Book", - Object[].class) - .getResultList(); -for (var result : results) { - String title = (String) result[0]; - String preamble = (String) result[1]; -} ----- - -This is bearable, but let's explore some other options. - -[[query-alt-result-types]] -==== Alternative generic result types - -If explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `javax.persistence.Tuple`, again, as specified by JPA. - -[source, java] -[%unbreakable] ----- -List tuples = - entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", - Tuple.class) - .getResultList(); -for (Tuple tuple : tuples) { - String title = tuple.get("title", String.class); - String preamble = tuple.get("preamble", String.class); -} ----- - -The names of the `Tuple` elements are determined by the aliases given to the projected items in the select list. -If no aliases are specified, the elements may be accessed by their position in the list, where the first item is assigned the position zero. - -As an extension to JPA, Hibernate lets us pass `Map` or `List` here: - -[source, java] -[%unbreakable] ----- -var results = - entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", - Map.class) - .getResultList(); -for (var map : results) { - String title = (String) tuple.get("title"); - String preamble = (String) tuple.get("preamble"); -} ----- -[source, java] -[%unbreakable] ----- -var results = - entityManager.createQuery("select title, left(book.text, 200) from Book", - List.class) - .getResultList(); -for (var list : results) { - String title = (String) list.get(0); - String preamble = (String) tuple.get(1); -} ----- - -Unfortunately, none of the types `Object[]`, `List`, `Map`, nor `Tuple` lets us access an individual item in a result tuple without explicitly specifying the type of the item, either using a typecast in the case of `Object[]`, `List`, or `Map`, or by passing the class object to `get()` in the case of `Tuple`. -But there's another option, as we're about to see. - -[NOTE] -==== -Actually, `Tuple` really exists to service the criteria query API, and in that context it _does_ enable truly typesafe access to query results. -==== - -[[select-new]] -==== Instantiation - -Simplifying slightly, the BNF for a projected item is: - -[[select-item-bnf]] -[source, antlrv4] ----- -selection - : (expression | instantiation) alias? - -instantiation - : "NEW" instantiationTarget "(" selection ("," selection)* ")" - -alias - : "AS"? identifier ----- - -Where the list of ``selection``s in an `instantiation` is essentially a nested projection list. - -So there's a special expression type that's only legal in the select clause: the `instantiation` rule in the BNF above. -// Let's see what it does. - -This JPA-standard `select new` construct packages the query results into a user-written Java class instead of an array. - -[[select-clause-dynamic-instantiation-example]] -[source, java] -[%unbreakable] ----- -record BookSummary(String title, String summary) {} - -List results = - entityManager.createQuery("select new BookSummary(title, left(book.text, 200)) from Book", - BookSummary.class) - .getResultList(); -for (var result : results) { - String title = result.title(); - String preamble = result.summary(); -} ----- - -The class must have a matching constructor. - -[IMPORTANT] -==== -This class does not need to be mapped or annotated in any way. - -Even if the class _is_ an entity class, the resulting instances are _not_ managed entities and are _not_ associated with the session. -==== - -But Hibernate 6 goes one better and makes the `select new` syntax optional. - -[[select-clause-implicit-instantiation-example]] -[source, java] -[%unbreakable] ----- -record BookSummary(String title, String summary) {} - -List results = - entityManager.createQuery("select title, left(book.text, 200) from Book", - BookSummary.class) - .getResultList(); -for (var result : results) { - String title = result.title(); - String preamble = result.summary(); -} ----- - -In the past, this functionality required more ceremony. - -[cols="25,~,~,^15"] -|=== -| Result type | Legacy syntax | Streamlined syntax | JPA standard - -| `Map` | `select new map(x, y)` | `select x, y` | ✖/✖ -| `List` | `select new list(x, y)` | `select x, y` | ✖/✖ -| Arbitrary class `Record` | `select new Record(x, y)` | `select x, y` | ✔/✖ -|=== - - [[distinct]] ==== Duplicate removal From 73de885deb7d04eabc931b2e42f3fe2737c5a0fb Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 29 May 2023 12:39:21 +0200 Subject: [PATCH 5/9] HHH-16710 allow Map and List instantiation for native SQL queries --- .../AbstractSharedSessionContract.java | 10 +++- .../jpa/spi/NativeQueryListTransformer.java | 23 ++++++++++ .../jpa/spi/NativeQueryMapTransformer.java | 30 ++++++++++++ .../jpa/spi/NativeQueryTupleTransformer.java | 6 ++- .../query/hql/ImplicitInstantiationTest.java | 46 +++++++++++++++++++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java create mode 100644 hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 60299bceca61..50f587aae292 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -12,6 +12,7 @@ import java.sql.SQLException; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; import java.util.UUID; import java.util.function.Function; @@ -44,6 +45,8 @@ import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.Work; import org.hibernate.jdbc.WorkExecutorVisitable; +import org.hibernate.jpa.spi.NativeQueryListTransformer; +import org.hibernate.jpa.spi.NativeQueryMapTransformer; import org.hibernate.jpa.spi.NativeQueryTupleTransformer; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.procedure.ProcedureCall; @@ -849,7 +852,12 @@ protected void addResultType(Class resultClass, NativeQueryImplementor if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( new NativeQueryTupleTransformer() ); } - // TODO: handle Map, List as well + else if ( Map.class.equals( resultClass ) ) { + query.setTupleTransformer( new NativeQueryMapTransformer() ); + } + else if ( List.class.equals( resultClass ) ) { + query.setTupleTransformer( new NativeQueryListTransformer() ); + } else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java new file mode 100644 index 000000000000..8d1012b71bb6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ +package org.hibernate.jpa.spi; + +import org.hibernate.query.TupleTransformer; + +import java.util.List; + +/** + * A {@link TupleTransformer} for handling {@link List} results from native queries. + * + * @author Gavin King + */ +public class NativeQueryListTransformer implements TupleTransformer> { + @Override + public List transformTuple(Object[] tuple, String[] aliases) { + return List.of( tuple ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java new file mode 100644 index 000000000000..f8619642c1f7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java @@ -0,0 +1,30 @@ +/* + * 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 . + */ +package org.hibernate.jpa.spi; + +import org.hibernate.query.TupleTransformer; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Locale.ROOT; + +/** + * A {@link TupleTransformer} for handling {@link Map} results from native queries. + * + * @author Gavin King + */ +public class NativeQueryMapTransformer implements TupleTransformer> { + @Override + public Map transformTuple(Object[] tuple, String[] aliases) { + Map map = new HashMap<>( aliases.length ); + for ( int i = 0; i < aliases.length; i++ ) { + map.put( aliases[i].toLowerCase(ROOT), tuple[i] ); + } + return map; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java index b7da9f2af687..bf1b5d8d4a78 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java @@ -18,6 +18,8 @@ import org.hibernate.query.TypedTupleTransformer; import org.hibernate.transform.ResultTransformer; +import static java.util.Locale.ROOT; + /** * A {@link ResultTransformer} for handling JPA {@link Tuple} results from native queries. * @@ -81,7 +83,7 @@ public NativeTupleImpl(Object[] tuple, String[] aliases) { final String alias = aliases[i]; if ( alias != null ) { aliasToValue.put( alias, tuple[i] ); - aliasReferences.put( alias.toLowerCase(), alias ); + aliasReferences.put( alias.toLowerCase(ROOT), alias ); } } size = tuple.length; @@ -96,7 +98,7 @@ public X get(String alias, Class type) { @Override public Object get(String alias) { - final String aliasReference = aliasReferences.get( alias.toLowerCase() ); + final String aliasReference = aliasReferences.get( alias.toLowerCase(ROOT) ); if ( aliasReference != null && aliasToValue.containsKey( aliasReference ) ) { return aliasToValue.get( aliasReference ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java index 8e09b9442ecb..796474bab36d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java @@ -128,6 +128,52 @@ public void testListInstantiationWithAlias(SessionFactoryScope scope) { ); } + @Test + public void testSqlTupleInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Tuple result = session.createNativeQuery("select id as id, upper(name) as name from thingy_table", Tuple.class) + .addSynchronizedEntityClass(Thing.class) + .getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testSqlMapInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Map result = session.createNativeQuery("select id as id, upper(name) as name from thingy_table", Map.class) + .addSynchronizedEntityClass(Thing.class) + .getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testSqlListInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + List result = session.createNativeQuery("select id as id, upper(name) as name from thingy_table", List.class) + .addSynchronizedEntityClass(Thing.class) + .getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Entity(name = "Thing") @Table(name = "thingy_table") public class Thing { From 2f9071c9b53627cf3a95de64d87828f9d535d0f3 Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 29 May 2023 12:41:23 +0200 Subject: [PATCH 6/9] fix javax package name + minor code example errors (spotted by @andrei-ivanov) --- .../src/main/asciidoc/querylanguage/Concepts.adoc | 8 ++++---- .../userguide/chapters/query/hql/QueryLanguage.adoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc index 703d2fbaec62..f69073d30926 100644 --- a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc @@ -601,7 +601,7 @@ for (var result : results) { This is bearable, but let's explore some other options. -JPA lets us specify that we want each query result packaged as an instance of `javax.persistence.Tuple`. +JPA lets us specify that we want each query result packaged as an instance of `jakarta.persistence.Tuple`. All we have to do is pass the class `Tuple` to `createQuery()`. [source, java] @@ -630,8 +630,8 @@ var results = Map.class) .getResultList(); for (var map : results) { - String title = (String) tuple.get("title"); - String preamble = (String) tuple.get("preamble"); + String title = (String) map.get("title"); + String preamble = (String) map.get("preamble"); } ---- [source, java] @@ -643,7 +643,7 @@ var results = .getResultList(); for (var list : results) { String title = (String) list.get(0); - String preamble = (String) tuple.get(1); + String preamble = (String) list.get(1); } ---- diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index ccbb9654875a..bddd69c85a45 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1939,7 +1939,7 @@ There's no need to bother with trying to represent a "tuple of length 1". But if there are multiple expressions in the select list then: - by default, each query result is packaged as an array of type `Object[]`, or -- if explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `javax.persistence.Tuple`. +- if explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `jakarta.persistence.Tuple`. [[hql-select-clause-projection-example]] //.Query results as lists From 560f76fefd9afeb3902cd214139cec2aca395a1c Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 29 May 2023 15:52:17 +0200 Subject: [PATCH 7/9] HHH-16710 constructor-based instantiation for native queries --- .../AbstractSharedSessionContract.java | 25 +++++-- .../internal/util/ReflectHelper.java | 7 ++ .../NativeQueryConstructorTransformer.java | 74 +++++++++++++++++++ .../jpa/spi/NativeQueryListTransformer.java | 3 + .../jpa/spi/NativeQueryMapTransformer.java | 3 + .../jpa/spi/NativeQueryTupleTransformer.java | 2 + .../internal/ConcreteSqmSelectQueryPlan.java | 7 +- .../query/hql/ImplicitInstantiationTest.java | 17 ++++- 8 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 50f587aae292..69d3dac1c68e 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -45,6 +45,7 @@ import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.Work; import org.hibernate.jdbc.WorkExecutorVisitable; +import org.hibernate.jpa.spi.NativeQueryConstructorTransformer; import org.hibernate.jpa.spi.NativeQueryListTransformer; import org.hibernate.jpa.spi.NativeQueryMapTransformer; import org.hibernate.jpa.spi.NativeQueryTupleTransformer; @@ -101,6 +102,7 @@ import jakarta.persistence.criteria.CriteriaUpdate; import static java.lang.Boolean.TRUE; +import static org.hibernate.internal.util.ReflectHelper.isClass; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.jpa.internal.util.FlushModeTypeHelper.getFlushModeType; @@ -850,19 +852,26 @@ public NativeQueryImplementor createNativeQuery(String sqlString, Class resultCl protected void addResultType(Class resultClass, NativeQueryImplementor query) { if ( Tuple.class.equals( resultClass ) ) { - query.setTupleTransformer( new NativeQueryTupleTransformer() ); + query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE ); } else if ( Map.class.equals( resultClass ) ) { - query.setTupleTransformer( new NativeQueryMapTransformer() ); + query.setTupleTransformer( NativeQueryMapTransformer.INSTANCE ); } else if ( List.class.equals( resultClass ) ) { - query.setTupleTransformer( new NativeQueryListTransformer() ); + query.setTupleTransformer( NativeQueryListTransformer.INSTANCE ); } else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); } else if ( resultClass != Object.class && resultClass != Object[].class ) { - query.addResultTypeClass( resultClass ); + if ( isClass( resultClass ) + && getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ) == null ) { + // not a basic type + query.setTupleTransformer( new NativeQueryConstructorTransformer<>( resultClass ) ); + } + else { + query.addResultTypeClass( resultClass ); + } } } @@ -884,7 +893,13 @@ public NativeQueryImplementor createNativeQuery(String sqlString, String @SuppressWarnings("unchecked") final NativeQueryImplementor query = createNativeQuery( sqlString, resultSetMappingName ); if ( Tuple.class.equals( resultClass ) ) { - query.setTupleTransformer( new NativeQueryTupleTransformer() ); + query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE ); + } + else if ( Map.class.equals( resultClass ) ) { + query.setTupleTransformer( NativeQueryMapTransformer.INSTANCE ); + } + else if ( List.class.equals( resultClass ) ) { + query.setTupleTransformer( NativeQueryListTransformer.INSTANCE ); } return query; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java index ad1ef9e87e4c..c1ef735c4336 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java @@ -898,4 +898,11 @@ else if (member instanceof Method) { throw new AssertionFailure("member should have been a method or field"); } } + + public static boolean isClass(Class resultClass) { + return !resultClass.isArray() + && !resultClass.isPrimitive() + && !resultClass.isEnum() + && !resultClass.isInterface(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java new file mode 100644 index 000000000000..01c95001c87c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java @@ -0,0 +1,74 @@ +/* + * 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 . + */ +package org.hibernate.jpa.spi; + +import org.hibernate.InstantiationException; +import org.hibernate.query.TupleTransformer; + +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * A {@link TupleTransformer} for handling {@link List} results from native queries. + * + * @author Gavin King + */ +public class NativeQueryConstructorTransformer implements TupleTransformer { + + private final Class resultClass; + private Constructor constructor; + + private Constructor constructor(Object[] elements) { + if ( constructor == null ) { + try { + // we cannot be sure of the "true" parameter types + // of the constructor we're looking for, so we need + // to do something a bit weird here: match on just + // the number of parameters + for ( final Constructor candidate : resultClass.getDeclaredConstructors() ) { + final Class[] parameterTypes = candidate.getParameterTypes(); + if ( parameterTypes.length == elements.length ) { + // found a candidate with the right number + // of parameters + if ( constructor == null ) { + constructor = resultClass.getDeclaredConstructor( parameterTypes ); + constructor.setAccessible( true ); + } + else { + // ambiguous, more than one constructor + // with the right number of parameters + constructor = null; + break; + } + } + } + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type ", resultClass, e ); + } + if ( constructor == null ) { + throw new InstantiationException( "Result class must have a single constructor with exactly " + + elements.length + " parameters", resultClass ); + } + } + return constructor; + } + + public NativeQueryConstructorTransformer(Class resultClass) { + this.resultClass = resultClass; + } + + @Override + public T transformTuple(Object[] tuple, String[] aliases) { + try { + return constructor( tuple ).newInstance( tuple ); + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type", resultClass, e ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java index 8d1012b71bb6..ce4636eb5017 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java @@ -16,6 +16,9 @@ * @author Gavin King */ public class NativeQueryListTransformer implements TupleTransformer> { + + public static final NativeQueryListTransformer INSTANCE = new NativeQueryListTransformer(); + @Override public List transformTuple(Object[] tuple, String[] aliases) { return List.of( tuple ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java index f8619642c1f7..478a241c6ae0 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java @@ -19,6 +19,9 @@ * @author Gavin King */ public class NativeQueryMapTransformer implements TupleTransformer> { + + public static final NativeQueryMapTransformer INSTANCE = new NativeQueryMapTransformer(); + @Override public Map transformTuple(Object[] tuple, String[] aliases) { Map map = new HashMap<>( aliases.length ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java index bf1b5d8d4a78..2e40a9e1a6a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java @@ -27,6 +27,8 @@ */ public class NativeQueryTupleTransformer implements ResultTransformer, TypedTupleTransformer { + public static final NativeQueryTupleTransformer INSTANCE = new NativeQueryTupleTransformer(); + @Override public Tuple transformTuple(Object[] tuple, String[] aliases) { return new NativeTupleImpl( tuple, aliases ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index b0bcf9ba2ef0..19bc1f6a3951 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -13,6 +13,7 @@ import jakarta.persistence.Tuple; import org.hibernate.AssertionFailure; +import org.hibernate.InstantiationException; import org.hibernate.ScrollMode; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -60,6 +61,7 @@ import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.sql.results.spi.RowTransformer; +import static org.hibernate.internal.util.ReflectHelper.isClass; import static org.hibernate.query.sqm.internal.QuerySqmImpl.CRITERIA_HQL_STRING; /** @@ -207,9 +209,12 @@ else if ( resultType == List.class ) { else if ( Map.class.equals( resultType ) ) { return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); } - else { + else if ( isClass( resultType ) ) { return new RowTransformerConstructorImpl<>( resultType, tupleMetadata ); } + else { + throw new InstantiationException( "Query result type is not instantiable", resultType ); + } } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java index 796474bab36d..cfa3beee8097 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java @@ -50,6 +50,21 @@ public void testRecordInstantiationWithoutAlias(SessionFactoryScope scope) { ); } + @Test + public void testSqlRecordInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Record result = session.createNativeQuery("select id, upper(name) as name from thingy_table", Record.class) + .addSynchronizedEntityClass(Thing.class) + .getSingleResult(); + assertEquals( result.id(), 1L ); + assertEquals( result.name(), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + @Test public void testTupleInstantiationWithAlias(SessionFactoryScope scope) { scope.inTransaction( @@ -163,7 +178,7 @@ public void testSqlListInstantiationWithoutAlias(SessionFactoryScope scope) { scope.inTransaction( session -> { session.persist(new Thing(1L, "thing")); - List result = session.createNativeQuery("select id as id, upper(name) as name from thingy_table", List.class) + List result = session.createNativeQuery("select id, upper(name) as name from thingy_table", List.class) .addSynchronizedEntityClass(Thing.class) .getSingleResult(); assertEquals( result.get(0), 1L ); From 7f6e70f9b2976e971d1d0ee84e8af73f5cb78932 Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 29 May 2023 18:51:09 +0200 Subject: [PATCH 8/9] HHH-16742 fix implementation of TupleMetadata fix issue when "same" selection item is assigned two different aliases --- .../query/spi/AbstractSelectionQuery.java | 73 +++++++++++++------ .../sql/results/internal/TupleMetadata.java | 45 +++++++----- 2 files changed, 76 insertions(+), 42 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index 8dffca663785..902e9317f127 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -11,7 +11,6 @@ import java.util.Calendar; import java.util.Collection; import java.util.Date; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -128,36 +127,62 @@ protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class r private TupleMetadata getTupleMetadata(List> selections) { if ( getQueryOptions().getTupleTransformer() == null ) { - return new TupleMetadata( buildTupleElementMap( selections ) ); + return new TupleMetadata( buildTupleElementArray( selections ), buildTupleAliasArray( selections ) ); } else { throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + - getQueryOptions().getTupleTransformer() + "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer: " + + getQueryOptions().getTupleTransformer() ); } } - private static Map, Integer> buildTupleElementMap(List> selections) { - final Map, Integer> tupleElementMap; - if ( selections.size() == 1 - && selections.get( 0 ).getSelectableNode() instanceof CompoundSelection ) { - final List> selectionItems = - selections.get( 0 ).getSelectableNode() - .getSelectionItems(); - tupleElementMap = new IdentityHashMap<>( selectionItems.size() ); - for ( int i = 0; i < selectionItems.size(); i++ ) { - tupleElementMap.put( selectionItems.get( i ), i ); + private static TupleElement[] buildTupleElementArray(List> selections) { + final TupleElement[] elements; + if ( selections.size() == 1 ) { + final SqmSelectableNode selectableNode = selections.get(0).getSelectableNode(); + if ( selectableNode instanceof CompoundSelection ) { + final List> selectionItems = selectableNode.getSelectionItems(); + elements = new TupleElement[ selectionItems.size() ]; + for ( int i = 0; i < selectionItems.size(); i++ ) { + elements[i] = selectionItems.get( i ); + } + } + else { + elements = new TupleElement[] { selectableNode }; + } + } + else { + elements = new TupleElement[ selections.size() ]; + for ( int i = 0; i < selections.size(); i++ ) { + elements[i] = selections.get(i).getSelectableNode(); + } + } + return elements; + } + + private static String[] buildTupleAliasArray(List> selections) { + final String[] elements; + if ( selections.size() == 1 ) { + final SqmSelectableNode selectableNode = selections.get(0).getSelectableNode(); + if ( selectableNode instanceof CompoundSelection ) { + final List> selectionItems = selectableNode.getSelectionItems(); + elements = new String[ selectionItems.size() ]; + for ( int i = 0; i < selectionItems.size(); i++ ) { + elements[i] = selectionItems.get( i ).getAlias(); + } + } + else { + elements = new String[] { selectableNode.getAlias() }; } } else { - tupleElementMap = new IdentityHashMap<>( selections.size() ); - for (int i = 0; i < selections.size(); i++ ) { - final SqmSelection selection = selections.get( i ); - tupleElementMap.put( selection.getSelectableNode(), i ); + elements = new String[ selections.size() ]; + for ( int i = 0; i < selections.size(); i++ ) { + elements[i] = selections.get(i).getAlias(); } } - return tupleElementMap; + return elements; } protected void applyOptions(NamedSqmQueryMemento memento) { @@ -173,10 +198,12 @@ protected void applyOptions(NamedSqmQueryMemento memento) { if ( memento.getParameterTypes() != null ) { for ( Map.Entry entry : memento.getParameterTypes().entrySet() ) { - final QueryParameterImplementor parameter = getParameterMetadata().getQueryParameter( entry.getKey() ); - final BasicType type = getSessionFactory().getTypeConfiguration() - .getBasicTypeRegistry() - .getRegisteredType( entry.getValue() ); + final QueryParameterImplementor parameter = + getParameterMetadata().getQueryParameter( entry.getKey() ); + final BasicType type = + getSessionFactory().getTypeConfiguration() + .getBasicTypeRegistry() + .getRegisteredType( entry.getValue() ); parameter.applyAnticipatedType( type ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleMetadata.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleMetadata.java index 0bed72c6bc19..94fa6966f54d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleMetadata.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleMetadata.java @@ -7,51 +7,58 @@ package org.hibernate.sql.results.internal; -import java.util.Arrays; -import java.util.Collections; +import jakarta.persistence.TupleElement; + import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import jakarta.persistence.TupleElement; + +import static java.util.Collections.unmodifiableMap; /** * Metadata about the tuple structure. * * @author Christian Beikov + * @author Gavin King */ public final class TupleMetadata { - private final Map, Integer> index; + private final TupleElement[] elements; + private final String[] aliases; private Map nameIndex; + private Map, Integer> elementIndex; private List> list; - public TupleMetadata(Map, Integer> index) { - this.index = index; + public TupleMetadata(TupleElement[] elements, String[] aliases) { + this.elements = elements; + this.aliases = aliases; } - public Integer get(TupleElement tupleElement) { - return index.get( tupleElement ); + public Integer get(TupleElement element) { + if ( elementIndex == null ) { + final Map, Integer> map = new IdentityHashMap<>( elements.length ); + for (int i = 0; i < elements.length; i++ ) { + map.put( elements[i], i ); + } + elementIndex = unmodifiableMap( map ); + } + return elementIndex.get( element ); } public Integer get(String name) { - Map nameIndex = this.nameIndex; if ( nameIndex == null ) { - nameIndex = new HashMap<>( index.size() ); - for ( Map.Entry, Integer> entry : index.entrySet() ) { - nameIndex.put( entry.getKey().getAlias(), entry.getValue() ); + final Map map = new HashMap<>( aliases.length ); + for ( int i = 0; i < aliases.length; i++ ) { + map.put( aliases[i], i ); } - this.nameIndex = nameIndex = Collections.unmodifiableMap( nameIndex ); + nameIndex = unmodifiableMap( map ); } return nameIndex.get( name ); } public List> getList() { - List> list = this.list; if ( list == null ) { - final TupleElement[] array = new TupleElement[index.size()]; - for ( Map.Entry, Integer> entry : index.entrySet() ) { - array[entry.getValue()] = entry.getKey(); - } - this.list = list = Collections.unmodifiableList( Arrays.asList( array ) ); + list = List.of( elements ); } return list; } From 31350cf12a50bc3999f0aa8e83d1129f17923fe2 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 5 Jun 2023 11:40:18 +0200 Subject: [PATCH 9/9] HHH-16747 nail down query result types and actually document the semantics --- .../AbstractSharedSessionContract.java | 2 +- .../metamodel/model/domain/JpaMetamodel.java | 4 +- .../org/hibernate/query/QueryProducer.java | 192 ++++++++++++++---- .../hql/internal/SemanticQueryBuilder.java | 64 ++++-- .../QueryInterpretationCacheDisabledImpl.java | 2 +- .../QueryInterpretationCacheStandardImpl.java | 65 ++++-- .../org/hibernate/query/results/Builders.java | 16 +- .../DynamicResultBuilderEntityCalculated.java | 7 +- .../implicit/ImplicitResultClassBuilder.java | 10 +- .../query/sql/internal/NativeQueryImpl.java | 5 + .../query/sql/spi/NativeQueryImplementor.java | 2 + .../sqm/tree/domain/AbstractSqmFrom.java | 5 +- .../results/ImplicitSelectWithJoinTests.java | 101 ++++++++- 13 files changed, 367 insertions(+), 108 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 69d3dac1c68e..50c976c69b1f 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -861,7 +861,7 @@ else if ( List.class.equals( resultClass ) ) { query.setTupleTransformer( NativeQueryListTransformer.INSTANCE ); } else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { - query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); + query.addEntity( resultClass, LockMode.READ ); } else if ( resultClass != Object.class && resultClass != Object[].class ) { if ( isClass( resultClass ) diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java index e6a13c7946cd..12a676d7c86b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java @@ -80,8 +80,8 @@ public interface JpaMetamodel extends Metamodel { /** * Returns a map that gives access to the enum literal expressions that can be used in queries. - * The key is the short-hand enum literal. The value is a map, from enum class to the actual enum value. - * This is needed for parsing short-hand enum literals that don't use FQNs. + * The key is the shorthand enum literal. The value is a map, from enum class to the actual enum value. + * This is needed for parsing shorthand enum literals that don't use FQNs. */ Map, Enum>> getAllowedEnumLiteralTexts(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java index 8528faa72f23..e645f5fc0efd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java @@ -29,11 +29,28 @@ public interface QueryProducer { /** * Create a {@link Query} instance for the given HQL query, or * HQL insert, update, or delete statement. + *

+ * If a query has no explicit {@code select} list, the select list + * is inferred: + *

    + *
  • if there is exactly one root entity in the {@code from} + * clause, then that root entity is the only element of the + * select list, or + *
  • otherwise, if there are multiple root entities in the + * {@code from} clause, then the select list contains every + * root entity and every non-{@code fetch} joined entity. + *
* * @apiNote Returns a raw {@code Query} type instead of a wildcard * type {@code Query}, to match the signature of the JPA method * {@link jakarta.persistence.EntityManager#createQuery(String)}. * + * @implNote This method interprets some queries with an implicit + * {@code select} list in a quite unintuitive way. In some future + * release, this method will be modified to throw an exception + * when passed a query with a missing {@code select}. For now, use + * {@link #createQuery(String, Class)} to avoid ambiguity. + * * @param queryString The HQL query * * @return The {@link Query} instance for manipulation and execution @@ -41,21 +58,47 @@ public interface QueryProducer { * @see jakarta.persistence.EntityManager#createQuery(String) * * @deprecated use {@link #createQuery(String, Class)}, - * {@link #createSelectionQuery} or {@link #createMutationQuery(String)} - * depending on intention + * {@link #createSelectionQuery(String, Class)}, or + * {@link #createMutationQuery(String)} depending on intention */ @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createQuery(String queryString); /** - * Create a typed {@link Query} instance for the given HQL query string. + * Create a typed {@link Query} instance for the given HQL query + * string and given query result type. + *
    + *
  • If the query has a single item in the {@code select} list, + * then the select item must be assignable to the given result + * type. + *
  • Otherwise, if there are multiple select items, then the + * select items will be packaged into an instance of the + * result type. The result type must have an appropriate + * constructor with parameter types matching the select items, + * or it must be one of the types {@code Object[]}, + * {@link java.util.List}, {@link java.util.Map}, or + * {@link jakarta.persistence.Tuple}. + *
+ *

+ * If a query has no explicit {@code select} list, the select list + * is inferred from the given query result type: + *

    + *
  • if the result type is an entity type, the query must have + * exactly one root entity in the {@code from} clause, it must + * be assignable to the result type, and the inferred select + * list will contain just that entity, or + *
  • otherwise, the select list contains every root entity and + * every non-{@code fetch} joined entity, and each query result + * will be packaged into an instance of the result type, just + * as specified above. + *
*

* The returned {@code Query} may be executed by calling * {@link Query#getResultList()} or {@link Query#getSingleResult()}. * * @param queryString The HQL query * @param resultClass The type of the query result - * @return The Query instance for manipulation and execution + * @return The {@link Query} instance for manipulation and execution * * @see jakarta.persistence.EntityManager#createQuery(String,Class) */ @@ -98,15 +141,25 @@ public interface QueryProducer { /** * Create a {@link NativeQuery} instance for the given native SQL query - * using implicit mapping to the specified Java type. - *

- * If the given class is an entity class, this method is equivalent to - * {@code createNativeQuery(sqlString).addEntity("alias1", resultClass)}. + * using an implicit mapping to the specified Java type. + *

    + *
  • If the given class is an entity class, this method is equivalent + * to {@code createNativeQuery(sqlString).addEntity(resultClass)}. + *
  • If the given class has a registered + * {@link org.hibernate.type.descriptor.java.JavaType}, then the + * query must return a result set with a single column whose + * {@code JdbcType} is compatible with that {@code JavaType}. + *
  • Otherwise, the select items will be packaged into an instance of + * the result type. The result type must have an appropriate + * constructor with parameter types matching the select items, or it + * must be one of the types {@code Object[]}, {@link java.util.List}, + * {@link java.util.Map}, or {@link jakarta.persistence.Tuple}. + *
* * @param sqlString The native (SQL) query string - * @param resultClass The Java entity type to map results to + * @param resultClass The Java type to map results to * - * @return The NativeQuery instance for manipulation and execution + * @return The {@link NativeQuery} instance for manipulation and execution * * @see jakarta.persistence.EntityManager#createNativeQuery(String,Class) */ @@ -114,13 +167,13 @@ public interface QueryProducer { /** * Create a {@link NativeQuery} instance for the given native SQL query - * using implicit mapping to the specified Java type. + * using an implicit mapping to the specified Java entity type. *

- * If the given class is an entity class, this method is equivalent to + * The given class must be an entity class. This method is equivalent to * {@code createNativeQuery(sqlString).addEntity(tableAlias, resultClass)}. * * @param sqlString Native (SQL) query string - * @param resultClass The Java entity type to map results to + * @param resultClass The Java entity class to map results to * @param tableAlias The table alias for columns in the result set * * @return The {@link NativeQuery} instance for manipulation and execution @@ -133,13 +186,13 @@ public interface QueryProducer { * Create a {@link NativeQuery} instance for the given native SQL query * using an explicit mapping to the specified Java type. *

- * The given result set mapping name must identify a mapping defined by a - * {@link jakarta.persistence.SqlResultSetMapping} annotation. + * The given result set mapping name must identify a mapping defined by + * a {@link jakarta.persistence.SqlResultSetMapping} annotation. * * @param sqlString The native (SQL) query string * @param resultSetMappingName The explicit result mapping name * - * @return The NativeQuery instance for manipulation and execution + * @return The {@link NativeQuery} instance for manipulation and execution * * @see jakarta.persistence.EntityManager#createNativeQuery(String,Class) * @see jakarta.persistence.SqlResultSetMapping @@ -153,8 +206,8 @@ public interface QueryProducer { * Create a {@link NativeQuery} instance for the given native SQL query * using an explicit mapping to the specified Java type. *

- * The given result set mapping name must identify a mapping defined by a - * {@link jakarta.persistence.SqlResultSetMapping} annotation. + * The given result set mapping name must identify a mapping defined by + * a {@link jakarta.persistence.SqlResultSetMapping} annotation. * * @param sqlString The native (SQL) query string * @param resultSetMappingName The explicit result mapping name @@ -167,21 +220,69 @@ public interface QueryProducer { NativeQuery createNativeQuery(String sqlString, String resultSetMappingName, Class resultClass); /** - * Create a {@link SelectionQuery} reference for the given HQL. - * - * Only valid for select queries - * - * @see jakarta.persistence.EntityManager#createQuery(String) + * Create a {@link SelectionQuery} reference for the given HQL + * {@code select} statement. + *

+ * If the statement has no explicit {@code select} list, the + * select list is inferred: + *

    + *
  • if there is exactly one root entity in the {@code from} + * clause, then that root entity is the only element of the + * select list, or + *
  • otherwise, if there are multiple root entities in the + * {@code from} clause, then the select list contains every + * root entity and every non-{@code fetch} joined entity. + *
+ * + * @implNote This method interprets some queries with an implicit + * {@code select} list in a quite unintuitive way. In some future + * release, this method will be modified to throw an exception + * when passed a query with a missing {@code select}. For now, use + * {@link #createSelectionQuery(String, Class)} to avoid ambiguity. * * @throws IllegalSelectQueryException if the given HQL query * is an insert, update or delete query + * + * @deprecated Use {@link #createSelectionQuery(String, Class)} */ + @Deprecated(since = "6.3") SelectionQuery createSelectionQuery(String hqlString); /** - * Create a {@link SelectionQuery} reference for the given HQL. - * - * Only valid for select queries + * Create a {@link SelectionQuery} instance for the given HQL query + * string and given query result type. + *
    + *
  • If the query has a single item in the {@code select} list, + * then the select item must be assignable to the given result + * type. + *
  • Otherwise, if there are multiple select items, then the + * select items will be packaged into an instance of the + * result type. The result type must have an appropriate + * constructor with parameter types matching the select items, + * or it must be one of the types {@code Object[]}, + * {@link java.util.List}, {@link java.util.Map}, or + * {@link jakarta.persistence.Tuple}. + *
+ *

+ * If a query has no explicit {@code select} list, the select list + * is inferred from the given query result type: + *

    + *
  • if the result type is an entity type, the query must have + * exactly one root entity in the {@code from} clause, it must + * be assignable to the result type, and the inferred select + * list will contain just that entity, or + *
  • otherwise, the select list contains every root entity and + * every non-{@code fetch} joined entity, and each query result + * will be packaged into an instance of the result type, just + * as specified above. + *
+ *

+ * The returned {@code Query} may be executed by calling + * {@link Query#getResultList()} or {@link Query#getSingleResult()}. + + * @param hqlString The HQL query as a string + * @param resultType The {@link Class} object representing the + * query result type * * @see jakarta.persistence.EntityManager#createQuery(String) * @@ -191,14 +292,15 @@ public interface QueryProducer { SelectionQuery createSelectionQuery(String hqlString, Class resultType); /** - * Create a {@link SelectionQuery} reference for the given Criteria. + * Create a {@link SelectionQuery} reference for the given + * {@link CriteriaQuery}. * * @see jakarta.persistence.EntityManager#createQuery(CriteriaQuery) */ SelectionQuery createSelectionQuery(CriteriaQuery criteria); /** - * Create a MutationQuery reference for the given HQL insert, + * Create a {@link MutationQuery} reference for the given HQL insert, * update, or delete statement. * * @throws IllegalMutationQueryException if the given HQL query @@ -234,7 +336,7 @@ public interface QueryProducer { * Create a typed {@link Query} instance for the given named query. * The named query might be defined in HQL or in native SQL. * - * @param name the name of a pre-defined, named query + * @param name the name of a predefined named query * * @return The {@link Query} instance for manipulation and execution * @@ -244,8 +346,7 @@ public interface QueryProducer { * * @see jakarta.persistence.EntityManager#createNamedQuery(String) * - * @deprecated use {@link #createNamedQuery(String, Class)} or - * {@link #createNamedMutationQuery(String)} + * @deprecated use {@link #createNamedQuery(String, Class)} */ @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createNamedQuery(String name); @@ -269,20 +370,29 @@ public interface QueryProducer { Query createNamedQuery(String name, Class resultClass); /** - * Create a {@link SelectionQuery} instance for the - * named {@link jakarta.persistence.NamedQuery} + * Create a {@link SelectionQuery} instance for the named + * {@link jakarta.persistence.NamedQuery}. * - * @throws IllegalSelectQueryException if the given HQL query is a select query + * @implNote This method interprets some queries with an implicit + * {@code select} list in a quite unintuitive way. In some future + * release, this method will be modified to throw an exception + * when passed a query with a missing {@code select}. For now, use + * {@link #createNamedSelectionQuery(String, Class)} to avoid + * ambiguity. + * + * @throws IllegalSelectQueryException if the given HQL query is not a select query * @throws UnknownNamedQueryException if no query has been defined with the given name + * + * @deprecated use {@link #createNamedSelectionQuery(String, Class)} */ + @Deprecated(since = "6.3") SelectionQuery createNamedSelectionQuery(String name); /** - * Create a {@link SelectionQuery} instance for the - * named {@link jakarta.persistence.NamedQuery} with the expected - * result-type + * Create a {@link SelectionQuery} instance for the named + * {@link jakarta.persistence.NamedQuery} with the given result type. * - * @throws IllegalSelectQueryException if the given HQL query is a select query + * @throws IllegalSelectQueryException if the given HQL query is not a select query * @throws UnknownNamedQueryException if no query has been defined with the given name */ SelectionQuery createNamedSelectionQuery(String name, Class resultType); @@ -301,7 +411,7 @@ public interface QueryProducer { /** * Create a {@link Query} instance for the named query. * - * @param queryName the name of a pre-defined, named query + * @param queryName the name of a predefined named query * * @return The {@link Query} instance for manipulation and execution * @@ -317,7 +427,7 @@ public interface QueryProducer { /** * Get a {@link NativeQuery} instance for a named native SQL query * - * @param name The name of the pre-defined query + * @param name The name of the predefined query * * @return The {@link NativeQuery} instance for manipulation and execution * @@ -329,7 +439,7 @@ public interface QueryProducer { /** * Get a {@link NativeQuery} instance for a named native SQL query * - * @param name The name of the pre-defined query + * @param name The name of the predefined query * * @return The {@link NativeQuery} instance for manipulation and execution * diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 072d3354b250..87b104a198da 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; +import org.hibernate.AssertionFailure; import org.hibernate.QueryException; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; @@ -1157,31 +1158,56 @@ public SqmQuerySpec visitQuery(HqlParser.QueryContext ctx) { } protected SqmSelectClause buildInferredSelectClause(SqmFromClause fromClause) { - // for now, this is slightly different to the legacy behavior where - // the root and each non-fetched-join was selected. For now, here, we simply - // select the root - final SqmSelectClause selectClause; + if ( fromClause.getNumberOfRoots() == 0 ) { + // should be impossible to get here + throw new AssertionFailure( "query has no 'select' clause, and no root entities"); + } - final boolean expectingArray = expectedResultType != null && expectedResultType.isArray(); - if ( expectingArray ) { - // triggers legacy interpretation of returning all roots - // and non-fetched joins - selectClause = new SqmSelectClause( - false, - creationContext.getNodeBuilder() - ); + final NodeBuilder nodeBuilder = creationContext.getNodeBuilder(); + + final SqmSelectClause selectClause; + final boolean singleEntityResult; + if ( expectedResultType == null ) { + // no result type was specified + // - if there is a single root entity return the entity, + // even if it has non-fetch joins (ugh!) + // - otherwise, return all entities in an Object[] array, + // including non-fetch joins + selectClause = new SqmSelectClause( false, nodeBuilder ); + singleEntityResult = fromClause.getNumberOfRoots() == 1; } else { - selectClause = new SqmSelectClause( - false, - fromClause.getNumberOfRoots(), - creationContext.getNodeBuilder() - ); + singleEntityResult = creationContext.getJpaMetamodel().findEntityType( expectedResultType ) != null; + if ( singleEntityResult ) { + // the result type is an entity class + if ( fromClause.getNumberOfRoots() > 1 ) { + // multiple root entities + throw new SemanticException( "query has no 'select' clause, and multiple root entities, but query result type is an entity class" + + " (specify an explicit 'select' list, or a different result type, for example, 'Object[].class')"); + } + else { + final SqmRoot sqmRoot = fromClause.getRoots().get(0); + if ( sqmRoot instanceof SqmCteRoot ) { + throw new SemanticException( "query has no 'select' clause, and the 'from' clause refers to a CTE, but query result type is an entity class" + + " (specify an explicit 'select' list)"); + } + else { + // exactly one root entity, return it + // (joined entities are not returned) + selectClause = new SqmSelectClause( false, 1, nodeBuilder ); + } + } + } + else { + // the result type is not an entity class + // return all root entities and non-fetch joins + selectClause = new SqmSelectClause( false, nodeBuilder ); + } } fromClause.visitRoots( (sqmRoot) -> { - selectClause.addSelection( new SqmSelection<>( sqmRoot, sqmRoot.getAlias(), creationContext.getNodeBuilder() ) ); - if ( expectingArray ) { + selectClause.addSelection( new SqmSelection<>( sqmRoot, sqmRoot.getAlias(), nodeBuilder) ); + if ( !singleEntityResult ) { applyJoinsToInferredSelectClause( sqmRoot, selectClause ); } } ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java index 598a71a3cbc8..eb563ff6d952 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java @@ -63,7 +63,7 @@ public void cacheNonSelectQueryPlan(Key key, NonSelectQueryPlan plan) { public HqlInterpretation resolveHqlInterpretation(String queryString, Class expectedResultType, Function> creator) { final StatisticsImplementor statistics = statisticsSupplier.get(); final boolean stats = statistics.isStatisticsEnabled(); - final long startTime = ( stats ) ? System.nanoTime() : 0L; + final long startTime = stats ? System.nanoTime() : 0L; final SqmStatement sqmStatement = creator.apply( queryString ); final DomainParameterXref domainParameterXref; diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java index bf8fef9b1ce4..72e43ce7962a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java @@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; -import jakarta.persistence.Tuple; import org.hibernate.internal.util.collections.BoundedConcurrentHashMap; import org.hibernate.query.QueryLogging; @@ -23,6 +22,7 @@ import org.hibernate.query.sql.spi.ParameterInterpretation; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.stat.spi.StatisticsImplementor; import org.jboss.logging.Logger; @@ -99,6 +99,45 @@ public void cacheNonSelectQueryPlan(Key key, NonSelectQueryPlan plan) { log.tracef( "QueryPlan#cacheNonSelectQueryPlan(%s)", key ); } + private static String typedKey(String queryString, Class expectedResultType) { + return expectedResultType.getName() + ":" + queryString; + } + + private void putInCache(String queryString, Class expectedResultType, HqlInterpretation hqlInterpretation) { + final SqmStatement statement = hqlInterpretation.getSqmStatement(); + if ( statement instanceof SqmSelectStatement && expectedResultType != null ) { + hqlInterpretationCache.put( typedKey( queryString, expectedResultType ), hqlInterpretation); + } + else { + hqlInterpretationCache.put( queryString, hqlInterpretation ); + } + } + + private HqlInterpretation getFromCache(String queryString, Class expectedResultType) { + if ( expectedResultType != null ) { + final HqlInterpretation typedHqlInterpretation = + hqlInterpretationCache.get( typedKey( queryString, expectedResultType ) ); + if ( typedHqlInterpretation != null ) { + return typedHqlInterpretation; + } + } + + final HqlInterpretation hqlInterpretation = hqlInterpretationCache.get( queryString ); + if ( hqlInterpretation != null ) { + final SqmStatement statement = hqlInterpretation.getSqmStatement(); + if ( statement instanceof SqmSelectStatement && expectedResultType != null ) { + final Class resultType = ((SqmSelectStatement) statement).getSelection().getJavaType(); + return expectedResultType.equals( resultType ) ? hqlInterpretation : null; + } + else { + return hqlInterpretation; + } + } + else { + return null; + } + } + @Override public HqlInterpretation resolveHqlInterpretation( String queryString, @@ -106,17 +145,7 @@ public HqlInterpretation resolveHqlInterpretation( Function> creator) { log.tracef( "QueryPlan#resolveHqlInterpretation( `%s` )", queryString ); - final String cacheKey; - if ( expectedResultType != null - && ( expectedResultType.isArray() || Tuple.class.isAssignableFrom( expectedResultType ) ) ) { - cacheKey = "multi_" + queryString; - } - else { - cacheKey = queryString; - } - - - final HqlInterpretation existing = hqlInterpretationCache.get( cacheKey ); + final HqlInterpretation existing = getFromCache( queryString, expectedResultType ); if ( existing != null ) { final StatisticsImplementor statistics = statisticsSupplier.get(); if ( statistics.isStatisticsEnabled() ) { @@ -124,10 +153,12 @@ public HqlInterpretation resolveHqlInterpretation( } return existing; } - - final HqlInterpretation hqlInterpretation = createHqlInterpretation( queryString, creator, statisticsSupplier ); - hqlInterpretationCache.put( cacheKey, hqlInterpretation ); - return hqlInterpretation; + else { + final HqlInterpretation hqlInterpretation = + createHqlInterpretation( queryString, creator, statisticsSupplier ); + putInCache( queryString, expectedResultType, hqlInterpretation ); + return hqlInterpretation; + } } protected static HqlInterpretation createHqlInterpretation( @@ -136,7 +167,7 @@ protected static HqlInterpretation createHqlInterpretation( Supplier statisticsSupplier) { final StatisticsImplementor statistics = statisticsSupplier.get(); final boolean stats = statistics.isStatisticsEnabled(); - final long startTime = ( stats ) ? System.nanoTime() : 0L; + final long startTime = stats ? System.nanoTime() : 0L; final SqmStatement sqmStatement = creator.apply( queryString ); final ParameterMetadataImplementor parameterMetadata; diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java b/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java index 211f659e4af7..f9367861999f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/Builders.java @@ -106,7 +106,7 @@ public static ResultBuilder converted( Class jdbcJavaType, AttributeConverter converter, SessionFactoryImplementor sessionFactory) { - return new DynamicResultBuilderBasicConverted( columnAlias, domainJavaType, jdbcJavaType, converter, sessionFactory ); + return new DynamicResultBuilderBasicConverted<>( columnAlias, domainJavaType, jdbcJavaType, converter, sessionFactory ); } public static ResultBuilder converted( @@ -123,7 +123,7 @@ public static ResultBuilder converted( Class jdbcJavaType, Class> converterJavaType, SessionFactoryImplementor sessionFactory) { - return new DynamicResultBuilderBasicConverted( columnAlias, domainJavaType, jdbcJavaType, converterJavaType, sessionFactory ); + return new DynamicResultBuilderBasicConverted<>( columnAlias, domainJavaType, jdbcJavaType, converterJavaType, sessionFactory ); } public static ResultBuilderBasicValued scalar(int position) { @@ -233,10 +233,8 @@ public static DynamicResultBuilderEntityCalculated entityCalculated( String entityName, LockMode explicitLockMode, SessionFactoryImplementor sessionFactory) { - final RuntimeMetamodels runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); - final EntityMappingType entityMapping = runtimeMetamodels.getEntityMappingType( entityName ); - - return new DynamicResultBuilderEntityCalculated( entityMapping, tableAlias, explicitLockMode, sessionFactory ); + final EntityMappingType entityMapping = sessionFactory.getRuntimeMetamodels().getEntityMappingType( entityName ); + return new DynamicResultBuilderEntityCalculated( entityMapping, tableAlias, explicitLockMode ); } public static DynamicFetchBuilderLegacy fetch(String tableAlias, String ownerTableAlias, String joinPropertyName) { @@ -252,9 +250,9 @@ public static ResultBuilder resultClassBuilder( public static ResultBuilder resultClassBuilder( Class resultMappingClass, SessionFactoryImplementor sessionFactory) { - final MappingMetamodelImplementor mappingMetamodel = sessionFactory - .getRuntimeMetamodels() - .getMappingMetamodel(); + final MappingMetamodelImplementor mappingMetamodel = + sessionFactory.getRuntimeMetamodels() + .getMappingMetamodel(); final EntityMappingType entityMappingType = mappingMetamodel.findEntityDescriptor( resultMappingClass ); if ( entityMappingType != null ) { // the resultClass is an entity diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityCalculated.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityCalculated.java index b082fb69ea9b..2d6258ed9315 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityCalculated.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityCalculated.java @@ -9,7 +9,6 @@ import java.util.function.BiFunction; import org.hibernate.LockMode; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.query.NativeQuery; import org.hibernate.query.results.DomainResultCreationStateImpl; @@ -35,18 +34,14 @@ public class DynamicResultBuilderEntityCalculated implements DynamicResultBuilde private final String tableAlias; private final LockMode explicitLockMode; - private final SessionFactoryImplementor sessionFactory; - public DynamicResultBuilderEntityCalculated( EntityMappingType entityMapping, String tableAlias, - LockMode explicitLockMode, - SessionFactoryImplementor sessionFactory) { + LockMode explicitLockMode) { this.entityMapping = entityMapping; this.navigablePath = new NavigablePath( entityMapping.getEntityName() ); this.tableAlias = tableAlias; this.explicitLockMode = explicitLockMode; - this.sessionFactory = sessionFactory; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java index 7726e02a53a8..ea8613d15d31 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/implicit/ImplicitResultClassBuilder.java @@ -45,11 +45,13 @@ public DomainResult buildResult( DomainResultCreationState domainResultCreationState) { assert resultPosition == 0; - final SessionFactoryImplementor sessionFactory = domainResultCreationState.getSqlAstCreationState() - .getCreationContext() - .getSessionFactory(); + final SessionFactoryImplementor sessionFactory = + domainResultCreationState.getSqlAstCreationState() + .getCreationContext() + .getSessionFactory(); final TypeConfiguration typeConfiguration = sessionFactory.getTypeConfiguration(); - final SqlExpressionResolver sqlExpressionResolver = domainResultCreationState.getSqlAstCreationState().getSqlExpressionResolver(); + final SqlExpressionResolver sqlExpressionResolver = + domainResultCreationState.getSqlAstCreationState().getSqlExpressionResolver(); final int jdbcResultPosition = 1; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index ec52a8db1fa3..beb5ba8f2d3c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -1014,6 +1014,11 @@ public NativeQueryImplementor addEntity(@SuppressWarnings("rawtypes") Class e return addEntity( entityType.getName() ); } + @Override + public NativeQueryImplementor addEntity(Class entityType, LockMode lockMode) { + return addEntity( StringHelper.unqualify( entityType.getName() ), entityType.getName(), lockMode); + } + @Override public NativeQueryImplementor addEntity(String tableAlias, @SuppressWarnings("rawtypes") Class entityClass) { return addEntity( tableAlias, entityClass.getName() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java index 65afbf6e7dd7..c621fc3e8f71 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java @@ -112,6 +112,8 @@ NativeQueryImplementor addScalar( @Override NativeQueryImplementor addEntity(@SuppressWarnings("rawtypes") Class entityType); + NativeQueryImplementor addEntity(Class entityType, LockMode lockMode); + @Override NativeQueryImplementor addEntity(String tableAlias, @SuppressWarnings("rawtypes") Class entityType); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 06869348a4cf..54d7c400241c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -238,7 +238,7 @@ else if ( lhs instanceof SqmEntityJoin ) { @Override public boolean hasJoins() { - return !( joins == null || joins.isEmpty() ); + return joins != null && !joins.isEmpty(); } @Override @@ -330,7 +330,6 @@ public SqmSingularJoin join(SingularAttribute attribute) } @Override - @SuppressWarnings("unchecked") public SqmSingularJoin join(SingularAttribute attribute, JoinType jt) { final SqmSingularJoin join = buildSingularJoin( (SingularPersistentAttribute) attribute, SqmJoinType.from( jt ), false ); addSqmJoin( join ); @@ -646,7 +645,6 @@ public SqmSingularJoin fetch(SingularAttribute attribute) } @Override - @SuppressWarnings("unchecked") public SqmSingularJoin fetch(SingularAttribute attribute, JoinType jt) { final SqmSingularJoin join = buildSingularJoin( (SingularPersistentAttribute) attribute, @@ -663,7 +661,6 @@ public SqmAttributeJoin fetch(PluralAttribute attribu } @Override - @SuppressWarnings("unchecked") public SqmAttributeJoin fetch(PluralAttribute attribute, JoinType jt) { return buildJoin( (PluralPersistentAttribute) attribute, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitSelectWithJoinTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitSelectWithJoinTests.java index 04614ad2dc58..0e684ab7b217 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitSelectWithJoinTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/results/ImplicitSelectWithJoinTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; /** * @author Steve Ebersole @@ -34,6 +35,7 @@ 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; + private static final String HQL3 = "from Product q join q.vendor w, Product p join p.vendor v where v.name like '%Steve%' and w.name like '%Gavin%'"; @Test public void testNoExpectedType(SessionFactoryScope scope) { @@ -79,8 +81,93 @@ public void testProductResult(SessionFactoryScope scope) { } ); } + @Test + public void testArrayResultNoResultType(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final SelectionQuery query = session.createSelectionQuery( HQL3 ); + + { + final List results = query.list(); + assertThat( results ).hasSize( 1 ); + final Object result = results.get( 0 ); + assertThat( result ).isNotNull(); + assertInstanceOf( Object[].class, result ); + assertThat( (Object[]) result ).hasSize(4); + assertThat( (Object[]) result ).hasExactlyElementsOfTypes(Product.class, Vendor.class, Product.class, Vendor.class); + } + + { + final ScrollableResults results = query.scroll(); + assertThat( results.next() ).isTrue(); + final Object result = results.get(); + assertThat( result ).isNotNull(); + assertInstanceOf( Object[].class, result ); + assertThat( (Object[]) result ).hasSize(4); + assertThat( (Object[]) result ).hasExactlyElementsOfTypes(Product.class, Vendor.class, Product.class, Vendor.class); + assertThat( results.next() ).isFalse(); + } + } ); + + // frankly, this would be more consistent and more backward-compatible +// scope.inTransaction( (session) -> { +// final SelectionQuery query = session.createSelectionQuery( HQL ); +// +// { +// final List results = query.list(); +// assertThat( results ).hasSize( 1 ); +// final Object result = results.get( 0 ); +// assertThat( result ).isNotNull(); +// assertInstanceOf( Object[].class, result ); +// assertThat( (Object[]) result ).hasSize(2); +// assertThat( (Object[]) result ).hasExactlyElementsOfTypes(Product.class, Vendor.class); +// } +// +// { +// final ScrollableResults results = query.scroll(); +// assertThat( results.next() ).isTrue(); +// final Object result = results.get(); +// assertThat( result ).isNotNull(); +// assertInstanceOf( Object[].class, result ); +// assertThat( (Object[]) result ).hasSize(2); +// assertThat( (Object[]) result ).hasExactlyElementsOfTypes(Product.class, Vendor.class); +// assertThat( results.next() ).isFalse(); +// } +// } ); + } + @Test public void testArrayResult(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final SelectionQuery query = session.createSelectionQuery( HQL3, Object[].class ); + + { + final List results = query.list(); + assertThat( results ).hasSize( 1 ); + final Object[] result = results.get( 0 ); + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 4 ); + assertThat( result[ 0 ] ).isNotNull(); + assertThat( result[ 1 ] ).isNotNull(); + assertThat( result[ 2 ] ).isNotNull(); + assertThat( result[ 3 ] ).isNotNull(); + assertThat( result ).hasExactlyElementsOfTypes(Product.class, Vendor.class, Product.class, Vendor.class); + } + + { + final ScrollableResults results = query.scroll(); + assertThat( results.next() ).isTrue(); + final Object[] result = results.get(); + assertThat( results.next() ).isFalse(); + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 4 ); + assertThat( result[ 0 ] ).isNotNull(); + assertThat( result[ 1 ] ).isNotNull(); + assertThat( result[ 2 ] ).isNotNull(); + assertThat( result[ 3 ] ).isNotNull(); + assertThat( result ).hasExactlyElementsOfTypes(Product.class, Vendor.class, Product.class, Vendor.class); + } + } ); + scope.inTransaction( (session) -> { final SelectionQuery query = session.createSelectionQuery( HQL, Object[].class ); @@ -92,6 +179,7 @@ public void testArrayResult(SessionFactoryScope scope) { assertThat( result ).hasSize( 2 ); assertThat( result[ 0 ] ).isNotNull(); assertThat( result[ 1 ] ).isNotNull(); + assertThat( result ).hasExactlyElementsOfTypes(Product.class, Vendor.class); } { @@ -103,6 +191,7 @@ public void testArrayResult(SessionFactoryScope scope) { assertThat( result ).hasSize( 2 ); assertThat( result[ 0 ] ).isNotNull(); assertThat( result[ 1 ] ).isNotNull(); + assertThat( result ).hasExactlyElementsOfTypes(Product.class, Vendor.class); } } ); } @@ -161,10 +250,14 @@ public void testExplicitSingleSelectionProductResult(SessionFactoryScope scope) @BeforeEach public void prepareTestData(SessionFactoryScope scope) { scope.inTransaction( (session) -> { - final Vendor vendor = new Vendor( 1, "Steve's Curios", "Acme Corp." ); - final Product product = new Product( 10, UUID.fromString( "53886a8a-7082-4879-b430-25cb94415be8" ), vendor ); - session.persist( vendor ); - session.persist( product ); + final Vendor steve = new Vendor( 1, "Steve's Curios", "Acme Corp." ); + final Product product1 = new Product( 10, UUID.fromString( "53886a8a-7082-4879-b430-25cb94415be8" ), steve ); + final Vendor gavin = new Vendor( 2, "Gavin & Associates", "Acme Corp." ); + final Product product2 = new Product( 11, UUID.fromString( "53886a8b-3083-4879-b431-25cb95515be9" ), gavin ); + session.persist( steve ); + session.persist( product1 ); + session.persist( gavin ); + session.persist( product2 ); } ); }