HHH-16710 implicit instantiation of record classes
This commit is contained in:
parent
87a2b967c7
commit
72f03d9d0f
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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,21 +105,28 @@ 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 ) ) {
|
||||
else {
|
||||
final SqmSelectStatement<?> select = (SqmSelectStatement<?>) statement;
|
||||
final List<SqmSelection<?>> selections =
|
||||
( (SqmSelectStatement<?>) statement ).getQueryPart()
|
||||
.getFirstQuerySpec()
|
||||
.getSelectClause()
|
||||
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 ) );
|
||||
}
|
||||
|
@ -129,10 +137,6 @@ public abstract class AbstractSelectionQuery<R>
|
|||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<TupleElement<?>, Integer> buildTupleElementMap(List<SqmSelection<?>> selections) {
|
||||
final Map<TupleElement<?>, Integer> tupleElementMap;
|
||||
|
@ -263,54 +267,51 @@ 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 ( 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 ( selections.size() == 1 ) {
|
||||
// we have one item in the select list,
|
||||
// the type has to match (no instantiation)
|
||||
final SqmSelection<?> sqmSelection = selections.get(0);
|
||||
|
||||
if ( sqmSelection.getSelectableNode() instanceof SqmParameter ) {
|
||||
final SqmParameter<?> sqmParameter = (SqmParameter<?>) sqmSelection.getSelectableNode();
|
||||
|
||||
// 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 ( sqmParameter.getNodeType() == null || sqmParameter.getNodeType().getExpressibleJavaType() == null ) {
|
||||
if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) {
|
||||
// we can't verify the result type up front
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( jpaQueryComplianceEnabled ) {
|
||||
return;
|
||||
}
|
||||
final boolean jpaQueryComplianceEnabled =
|
||||
sessionFactory.getSessionFactoryOptions()
|
||||
.getJpaCompliance()
|
||||
.isJpaQueryComplianceEnabled();
|
||||
if ( !jpaQueryComplianceEnabled ) {
|
||||
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;
|
||||
|
@ -319,46 +320,63 @@ 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;
|
||||
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<?> ) {
|
||||
jdbcType = ( (BasicDomainType<?>) sqmExpressible).getJdbcType();
|
||||
return ( (BasicDomainType<?>) sqmExpressible).getJdbcType();
|
||||
}
|
||||
else if ( sqmExpressible instanceof SqmPathSource<?> ) {
|
||||
final DomainType<?> domainType = ( (SqmPathSource<?>) sqmExpressible).getSqmPathType();
|
||||
if ( domainType instanceof BasicDomainType<?> ) {
|
||||
jdbcType = ( (BasicDomainType<?>) domainType ).getJdbcType();
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Types.TIME:
|
||||
if ( resultClass.isAssignableFrom( java.sql.Time.class ) ) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case Types.TIMESTAMP:
|
||||
if ( resultClass.isAssignableFrom( java.sql.Timestamp.class ) ) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throwQueryTypeMismatchException( resultClass, sqmExpressible );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static <T> void throwQueryTypeMismatchException(Class<T> resultClass, SqmExpressible<?> sqmExpressible) {
|
||||
|
|
|
@ -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,25 +178,29 @@ 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
|
||||
|
||||
final List<SqmSelection<?>> selections =
|
||||
sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections();
|
||||
if ( tupleMetadata != null ) {
|
||||
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 {
|
||||
if ( Tuple.class.equals( resultType ) ) {
|
||||
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
|
||||
}
|
||||
|
@ -204,17 +208,9 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
|
|||
return (RowTransformer<T>) new RowTransformerMapImpl( tupleMetadata );
|
||||
}
|
||||
else {
|
||||
throw new AssertionFailure( "Wrong result type for tuple handling: " + resultType );
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() ) {
|
||||
return Object[].class;
|
||||
}
|
||||
else if ( List.class.isAssignableFrom( expectedResultType ) ) {
|
||||
return expectedResultType;
|
||||
}
|
||||
else if ( isTupleResultClass( expectedResultType ) ) {
|
||||
return expectedResultType;
|
||||
}
|
||||
else {
|
||||
return Object[].class;
|
||||
}
|
||||
}
|
||||
else {
|
||||
final List<SqmSelection<?>> selections = sqm.getQuerySpec().getSelectClause().getSelections();
|
||||
return selections.size() == 1
|
||||
? selections.get(0).getNodeJavaType().getJavaTypeClass()
|
||||
: Object[].class;
|
||||
if ( selections.size() == 1 ) {
|
||||
if ( Object[].class.equals( expectedResultType ) ) {
|
||||
// for JPA compatibility
|
||||
return Object[].class;
|
||||
}
|
||||
else {
|
||||
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 {
|
||||
// for JPA compatibility
|
||||
return Object[].class;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>> {
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue