Skip to content

Commit

Permalink
HHH-17764 query result types and single-item selection lists
Browse files Browse the repository at this point in the history
- allow single-item auto-instantiation
- check the type of the selection item against the given result type
  • Loading branch information
gavinking committed Feb 21, 2024
1 parent bb477c1 commit 24f6e7e
Show file tree
Hide file tree
Showing 19 changed files with 366 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.TypeMismatchException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.internal.EntityManagerMessageLogger;
Expand Down Expand Up @@ -654,9 +653,6 @@ public int executeUpdate() throws HibernateException {
catch (IllegalQueryOperationException e) {
throw new IllegalStateException( e );
}
catch (TypeMismatchException e) {
throw new IllegalArgumentException( e );
}
catch (HibernateException e) {
throw getSession().getExceptionConverter().convert( e );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.hibernate.LockOptions;
import org.hibernate.NonUniqueResultException;
import org.hibernate.ScrollMode;
import org.hibernate.TypeMismatchException;
import org.hibernate.UnknownProfileException;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
Expand Down Expand Up @@ -89,6 +88,8 @@
import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE;
import static org.hibernate.jpa.QueryHints.HINT_FOLLOW_ON_LOCKING;
import static org.hibernate.jpa.QueryHints.HINT_READONLY;
import static org.hibernate.query.sqm.internal.SqmUtil.isHqlTuple;
import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType;

/**
* @author Steve Ebersole
Expand All @@ -108,26 +109,26 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) {
}

protected TupleMetadata buildTupleMetadata(SqmStatement<?> statement, Class<R> resultType) {
if ( isInstantiableWithoutMetadata( resultType ) ) {
// no need to build metadata for instantiating tuples
return null;
}
else {
if ( statement instanceof SqmSelectStatement<?> ) {
final SqmSelectStatement<?> select = (SqmSelectStatement<?>) statement;
final List<SqmSelection<?>> selections =
select.getQueryPart().getFirstQuerySpec().getSelectClause()
.getSelections();
if ( Tuple.class.equals( resultType ) || selections.size() > 1 ) {
return getTupleMetadata( selections );
}
else {
// only one element in select list,
// we don't support instantiation
return null;
}
return isTupleMetadataRequired( resultType, selections.get(0) )
? getTupleMetadata( selections )
: null;
}
else {
return null;
}
}

private static <R> boolean isTupleMetadataRequired(Class<R> resultType, SqmSelection<?> selection) {
return isHqlTuple( selection )
|| !isInstantiableWithoutMetadata( resultType )
&& !isSelectionAssignableToResultType( selection, resultType );
}

private TupleMetadata getTupleMetadata(List<SqmSelection<?>> selections) {
if ( getQueryOptions().getTupleTransformer() == null ) {
return new TupleMetadata( buildTupleElementArray( selections ), buildTupleAliasArray( selections ) );
Expand Down Expand Up @@ -431,9 +432,6 @@ public List<R> list() {
catch (IllegalQueryOperationException e) {
throw new IllegalStateException( e );
}
catch (TypeMismatchException e) {
throw new IllegalArgumentException( e );
}
catch (HibernateException he) {
throw getSession().getExceptionConverter().convert( he, getQueryOptions().getLockOptions() );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
import java.util.Map;

import jakarta.persistence.Tuple;
import org.hibernate.AssertionFailure;
import org.hibernate.InstantiationException;
import org.hibernate.ScrollMode;
import org.hibernate.engine.spi.EntityHolder;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.SubselectFetch;
import org.hibernate.internal.EmptyScrollableResults;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.query.Query;
import org.hibernate.query.TupleTransformer;
Expand All @@ -43,6 +41,7 @@
import org.hibernate.sql.exec.spi.JdbcParametersList;
import org.hibernate.sql.exec.spi.JdbcSelectExecutor;
import org.hibernate.sql.results.internal.RowTransformerArrayImpl;
import org.hibernate.sql.results.internal.RowTransformerCheckingImpl;
import org.hibernate.sql.results.internal.RowTransformerConstructorImpl;
import org.hibernate.sql.results.internal.RowTransformerJpaTupleImpl;
import org.hibernate.sql.results.internal.RowTransformerListImpl;
Expand All @@ -56,7 +55,9 @@
import org.hibernate.sql.results.spi.RowTransformer;

import static org.hibernate.internal.util.ReflectHelper.isClass;
import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray;
import static org.hibernate.query.sqm.internal.QuerySqmImpl.CRITERIA_HQL_STRING;
import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType;

/**
* Standard Hibernate implementation of SelectQueryPlan for SQM-backed
Expand Down Expand Up @@ -203,60 +204,60 @@ private static SqmSelection<?> singleSelection(SqmSelectStatement<?> sqm) {
return selections.size() == 1 ? selections.get( 0 ) : null;
}

private static Class<?> selectionType(SqmSelection<?> selection) {
return selection != null && !selection.getSelectableNode().isCompoundSelection() ?
selection.getNodeJavaType().getJavaTypeClass()
: null;
}
private static final Map<Class<?>,Class<?>> WRAPPERS
= Map.of(
boolean.class, Boolean.class,
int.class, Integer.class,
long.class, Long.class,
short.class, Short.class,
byte.class, Byte.class,
float.class, Float.class,
double.class, Double.class,
char.class, Character.class
);

@SuppressWarnings("unchecked")
protected static <T> RowTransformer<T> determineRowTransformer(
SqmSelectStatement<?> sqm,
Class<T> resultType,
Class<T> resultClass,
TupleMetadata tupleMetadata,
QueryOptions queryOptions) {
if ( queryOptions.getTupleTransformer() != null ) {
return makeRowTransformerTupleTransformerAdapter( sqm, queryOptions );
}
else if ( resultType == null ) {
else if ( resultClass == null ) {
return RowTransformerStandardImpl.instance();
}
else {
final Class<T> resultType = (Class<T>)
WRAPPERS.getOrDefault( resultClass, resultClass );
final SqmSelection<?> selection = singleSelection( sqm );
if ( resultType.isArray() && resultType != selectionType( selection ) ) {
if ( isSelectionAssignableToResultType( selection, resultType ) ) {
return RowTransformerSingularReturnImpl.instance();
}
else if ( resultType.isArray() ) {
return (RowTransformer<T>) RowTransformerArrayImpl.instance();
}
else if ( resultType == List.class && resultType != selectionType( selection ) ) {
else if ( List.class.equals( resultType ) ) {
return (RowTransformer<T>) RowTransformerListImpl.instance();
}
else {
// NOTE : if we get here :
// 1) there is no TupleTransformer specified
// 2) an explicit result-type, other than an array or List, was specified

if ( tupleMetadata == null ) {
if ( selection != null ) {
return RowTransformerSingularReturnImpl.instance();
}
else {
throw new AssertionFailure( "Query defined multiple selections, should have had TupleMetadata" );
}
else if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerMapImpl( tupleMetadata );
}
else if ( isClass( resultType ) ) {
try {
return new RowTransformerConstructorImpl<>( resultType, tupleMetadata );
}
else {
if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerMapImpl( tupleMetadata );
}
else if ( isClass( resultType ) ) {
return new RowTransformerConstructorImpl<>( resultType, tupleMetadata );
}
else {
throw new InstantiationException( "Query result type is not instantiable", resultType );
}
catch (InstantiationException ie) {
return new RowTransformerCheckingImpl<>( resultType );
}
}
else {
return new RowTransformerCheckingImpl<>( resultType );
}
}
}

Expand All @@ -279,10 +280,7 @@ private static <T> RowTransformer<T> makeRowTransformerTupleTransformerAdapter(

@SuppressWarnings("unchecked")
TupleTransformer<T> tupleTransformer = (TupleTransformer<T>) queryOptions.getTupleTransformer();
return new RowTransformerTupleTransformerAdapter<T>(
ArrayHelper.toStringArray( aliases ),
tupleTransformer
);
return new RowTransformerTupleTransformerAdapter<>( toStringArray( aliases ), tupleTransformer );
}

@Override
Expand Down Expand Up @@ -424,10 +422,7 @@ private static CacheableSqmInterpretation buildCacheableSqmInterpretation(

final SqlAstTranslator<JdbcOperationQuerySelect> selectTranslator =
sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory()
.buildSelectTranslator(
sessionFactory,
sqmInterpretation.getSqlAst()
);
.buildSelectTranslator( sessionFactory, sqmInterpretation.getSqlAst() );

final Map<QueryParameterImplementor<?>, Map<SqmParameter<?>, List<JdbcParametersList>>> jdbcParamsXref
= SqmUtil.generateJdbcParamsXref( domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.ScrollMode;
import org.hibernate.TypeMismatchException;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.engine.spi.LoadQueryInfluencers;
Expand Down Expand Up @@ -692,9 +691,6 @@ public int executeUpdate() {
catch (IllegalQueryOperationException e) {
throw new IllegalStateException( e );
}
catch (TypeMismatchException e) {
throw new IllegalArgumentException( e );
}
catch (HibernateException e) {
throw getSession().getExceptionConverter().convert( e );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
import org.hibernate.sql.results.internal.TupleMetadata;
import org.hibernate.sql.results.spi.ResultsConsumer;
import org.hibernate.sql.results.spi.SingleResultConsumer;
import org.hibernate.type.descriptor.java.JavaType;

import static java.util.stream.Collectors.toList;
import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE;
Expand All @@ -82,6 +81,7 @@
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.internal.SqmUtil.isSelectionAssignableToResultType;
import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification;

/**
Expand Down Expand Up @@ -131,17 +131,14 @@ private Class<?> determineResultType(SqmSelectStatement<?> sqm) {
}
else {
final SqmSelection<?> selection = selections.get(0);
if ( selection!=null ) {
final JavaType<?> javaType = selection.getNodeJavaType();
if ( javaType != null) {
return javaType.getJavaTypeClass();
}
if ( isSelectionAssignableToResultType( selection, expectedResultType ) ) {
return selection.getNodeJavaType().getJavaTypeClass();
}
else {
// let's assume there's some
// way to instantiate it
return expectedResultType;
}
// 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 ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@
import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef;
import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper;
import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.from.SqmFrom;
import org.hibernate.query.sqm.tree.from.SqmJoin;
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
import org.hibernate.query.sqm.tree.from.SqmRoot;
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.query.sqm.tree.select.SqmSortSpecification;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.SqlTreeCreationException;
Expand All @@ -69,6 +71,7 @@
import org.hibernate.sql.exec.spi.JdbcParametersList;
import org.hibernate.type.JavaObjectType;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.internal.BasicTypeImpl;
import org.hibernate.type.internal.ConvertedBasicTypeImpl;
import org.hibernate.type.spi.TypeConfiguration;
Expand Down Expand Up @@ -601,6 +604,26 @@ static <T> JpaOrder sortSpecification(SqmSelectStatement<T> sqm, Order<? super T
}
}

public static boolean isSelectionAssignableToResultType(SqmSelection<?> selection, Class<?> expectedResultType) {
if ( expectedResultType == null
|| selection != null && selection.getSelectableNode() instanceof SqmParameter ) {
return true;
}
else if ( selection == null
|| !isHqlTuple( selection ) && selection.getSelectableNode().isCompoundSelection() ) {
return false;
}
else {
final JavaType<?> nodeJavaType = selection.getNodeJavaType();
return nodeJavaType != null
&& expectedResultType.isAssignableFrom( nodeJavaType.getJavaTypeClass() );
}
}

public static boolean isHqlTuple(SqmSelection<?> selection) {
return selection != null && selection.getSelectableNode() instanceof SqmTuple;
}

private static class CriteriaParameterCollector {
private Set<SqmParameter<?>> sqmParameters;
private Map<JpaCriteriaParameter<?>, List<SqmJpaCriteriaParameterWrapper<?>>> jpaCriteriaParamResolutions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.TypeMismatchException;
import org.hibernate.sql.results.spi.RowTransformer;

/**
* @author Gavin King
*/
public class RowTransformerCheckingImpl<R> implements RowTransformer<R> {

private final Class<R> type;

public RowTransformerCheckingImpl(Class<R> type) {
this.type = type;
}

@Override
@SuppressWarnings("unchecked")
public R transformRow(Object[] row) {
final Object result = row[0];
if ( result == null || type.isInstance( result ) ) {
return (R) result;
}
else {
throw new TypeMismatchException( "Result type is '" + type.getSimpleName()
+ "' but the query returned a '" + result.getClass().getSimpleName() + "'" );
}
}

@Override
public int determineNumberOfResultElements(int rawElementCount) {
return 1;
}
}
Loading

0 comments on commit 24f6e7e

Please sign in to comment.