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 50f587aae2..69d3dac1c6 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.id.uuid.StandardRandomStrategy; 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.CriteriaQuery; 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 abstract class AbstractSharedSessionContract implements SharedSessionCont 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 abstract class AbstractSharedSessionContract implements SharedSessionCont @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 ad1ef9e87e..c1ef735c43 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 @@ public final class ReflectHelper { 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 0000000000..01c95001c8 --- /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 8d1012b71b..ce4636eb50 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 @@ import java.util.List; * @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 f8619642c1..478a241c6a 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 @@ import static java.util.Locale.ROOT; * @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 bf1b5d8d4a..2e40a9e1a6 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 @@ import static java.util.Locale.ROOT; */ 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 b0bcf9ba2e..19bc1f6a39 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 java.util.Map; 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.internal.TupleMetadata; 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 @@ public class ConcreteSqmSelectQueryPlan implements SelectQueryPlan { 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 796474bab3..cfa3beee80 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 class ImplicitInstantiationTest { ); } + @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 class ImplicitInstantiationTest { 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 );