HHH-16710 implicit instantiation of record classes

This commit is contained in:
Gavin 2023-05-28 17:38:02 +02:00 committed by Gavin King
parent 87a2b967c7
commit 72f03d9d0f
9 changed files with 258 additions and 153 deletions

View File

@ -748,7 +748,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
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()
)

View File

@ -66,6 +66,7 @@ import org.hibernate.query.sqm.tree.select.SqmQueryGroup;
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 abstract class AbstractSelectionQuery<R>
super( session );
}
public static boolean isTupleResultClass(Class<?> resultType) {
return Tuple.class.isAssignableFrom( resultType )
|| Map.class.isAssignableFrom( resultType );
}
protected TupleMetadata buildTupleMetadata(SqmStatement<?> statement, Class<R> resultType) {
if ( resultType == null ) {
if ( isInstantiableWithoutMetadata( resultType ) ) {
// no need to build metadata for instantiating tuples
return null;
}
else if ( isTupleResultClass( resultType ) ) {
final List<SqmSelection<?>> selections =
( (SqmSelectStatement<?>) statement ).getQueryPart()
.getFirstQuerySpec()
.getSelectClause()
.getSelections();
if ( getQueryOptions().getTupleTransformer() == null ) {
return new TupleMetadata( buildTupleElementMap( selections ) );
}
else {
throw new IllegalArgumentException(
"Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " +
getQueryOptions().getTupleTransformer()
);
}
}
else {
return null;
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;
}
}
}
private TupleMetadata getTupleMetadata(List<SqmSelection<?>> selections) {
if ( getQueryOptions().getTupleTransformer() == null ) {
return new TupleMetadata( buildTupleElementMap( selections ) );
}
else {
throw new IllegalArgumentException(
"Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " +
getQueryOptions().getTupleTransformer()
);
}
}
@ -263,55 +267,52 @@ public abstract class AbstractSelectionQuery<R>
SqmQuerySpec<T> querySpec,
Class<T> expectedResultClass,
SessionFactoryImplementor sessionFactory) {
if ( expectedResultClass == null || expectedResultClass == Object.class ) {
// nothing to check
return;
}
if ( !isResultTypeAlwaysAllowed( expectedResultClass ) ) {
final List<SqmSelection<?>> 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);
final List<SqmSelection<?>> 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 );
// 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;
}
}
else {
throw new QueryTypeMismatchException( errorMessage );
final boolean jpaQueryComplianceEnabled =
sessionFactory.getSessionFactoryOptions()
.getJpaCompliance()
.isJpaQueryComplianceEnabled();
if ( !jpaQueryComplianceEnabled ) {
verifyResultType( expectedResultClass, sqmSelection.getNodeType() );
}
}
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;
}
}
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 <T> boolean isResultTypeAlwaysAllowed(Class<T> expectedResultClass) {
return expectedResultClass == null
|| expectedResultClass == Object.class
|| expectedResultClass == List.class
|| expectedResultClass == Tuple.class
|| expectedResultClass.isArray();
}
protected static <T> void verifyResultType(Class<T> resultClass, SqmExpressible<?> sqmExpressible) {
assert sqmExpressible != null;
final JavaType<?> expressibleJavaType = sqmExpressible.getExpressibleJavaType();
@ -319,48 +320,65 @@ public abstract class AbstractSelectionQuery<R>
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();
}
}
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;
}
}
}
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
private static <T> boolean isMatchingDateType(
Class<?> javaTypeClass,
Class<T> 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 <T> boolean isMatchingDateJdbcType(Class<T> resultClass, JdbcType jdbcType) {
if ( jdbcType != null ) {
switch ( jdbcType.getDefaultSqlTypeCode() ) {
case Types.DATE:
if ( resultClass.isAssignableFrom( java.sql.Date.class ) ) {
return true;
}
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;
}
}
return false;
}
private static <T> void throwQueryTypeMismatchException(Class<T> resultClass, SqmExpressible<?> sqmExpressible) {
final String errorMessage = String.format(
"Specified result type [%s] did not match Query selection type [%s] - multiple selections: use Tuple or array",

View File

@ -23,7 +23,6 @@ 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.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.JdbcParametersList;
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 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
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<T>) RowTransformerArrayImpl.instance();
}
else if ( List.class.equals( resultType ) ) {
else if ( resultType == List.class ) {
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, 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<SqmSelection<?>> selections =
sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections();
if ( tupleMetadata != null ) {
if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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 <T> RowTransformer<T> makeRowTransformerTupleTransformerAdapter(

View File

@ -60,6 +60,7 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter;
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 class SqmSelectionQueryImpl<R> extends AbstractSelectionQuery<R> implemen
}
private Class<?> determineResultType(SqmSelectStatement<?> sqm) {
if ( expectedResultType != null ) {
if ( expectedResultType.isArray() ) {
final List<SqmSelection<?>> 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<SqmSelection<?>> selections = sqm.getQuerySpec().getSelectClause().getSelections();
return selections.size() == 1
? selections.get(0).getNodeJavaType().getJavaTypeClass()
: Object[].class;
// for JPA compatibility
return Object[].class;
}
}

View File

@ -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<T> implements RowTransformer<T> {
private final Class<T> type;
private final TupleMetadata tupleMetadata;
private final Constructor<T> constructor;
public RowTransformerConstructorImpl(Class<T> type, TupleMetadata tupleMetadata) {
this.type = type;
this.tupleMetadata = tupleMetadata;
final List<TupleElement<?>> 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;
}
}

View File

@ -11,9 +11,9 @@ import org.hibernate.sql.results.spi.RowTransformer;
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<T> implements RowTransformer<List<Object>> {
/**

View File

@ -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.List;
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<Map<String,Object>> {
private final TupleMetadata tupleMetadata;

View File

@ -29,9 +29,4 @@ public class RowTransformerTupleTransformerAdapter<T> implements RowTransformer<
assert aliases == null || row.length == aliases.length;
return tupleTransformer.transformTuple( row, aliases );
}
@Override
public int determineNumberOfResultElements(int rawElementCount) {
return rawElementCount;
}
}

View File

@ -22,6 +22,34 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@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(