Skip to content

Commit acf9495

Browse files
committed
HHH-16710 constructor-based instantiation for native queries
1 parent 280df7c commit acf9495

File tree

8 files changed

+131
-7
lines changed

8 files changed

+131
-7
lines changed

hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.hibernate.jdbc.ReturningWork;
4646
import org.hibernate.jdbc.Work;
4747
import org.hibernate.jdbc.WorkExecutorVisitable;
48+
import org.hibernate.jpa.spi.NativeQueryConstructorTransformer;
4849
import org.hibernate.jpa.spi.NativeQueryListTransformer;
4950
import org.hibernate.jpa.spi.NativeQueryMapTransformer;
5051
import org.hibernate.jpa.spi.NativeQueryTupleTransformer;
@@ -101,6 +102,7 @@
101102
import jakarta.persistence.criteria.CriteriaUpdate;
102103

103104
import static java.lang.Boolean.TRUE;
105+
import static org.hibernate.internal.util.ReflectHelper.isClass;
104106
import static org.hibernate.internal.util.StringHelper.isEmpty;
105107
import static org.hibernate.internal.util.StringHelper.isNotEmpty;
106108
import static org.hibernate.jpa.internal.util.FlushModeTypeHelper.getFlushModeType;
@@ -850,19 +852,26 @@ public NativeQueryImplementor createNativeQuery(String sqlString, Class resultCl
850852

851853
protected <T> void addResultType(Class<T> resultClass, NativeQueryImplementor<T> query) {
852854
if ( Tuple.class.equals( resultClass ) ) {
853-
query.setTupleTransformer( new NativeQueryTupleTransformer() );
855+
query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE );
854856
}
855857
else if ( Map.class.equals( resultClass ) ) {
856-
query.setTupleTransformer( new NativeQueryMapTransformer() );
858+
query.setTupleTransformer( NativeQueryMapTransformer.INSTANCE );
857859
}
858860
else if ( List.class.equals( resultClass ) ) {
859-
query.setTupleTransformer( new NativeQueryListTransformer() );
861+
query.setTupleTransformer( NativeQueryListTransformer.INSTANCE );
860862
}
861863
else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) {
862864
query.addEntity( "alias1", resultClass.getName(), LockMode.READ );
863865
}
864866
else if ( resultClass != Object.class && resultClass != Object[].class ) {
865-
query.addResultTypeClass( resultClass );
867+
if ( isClass( resultClass )
868+
&& getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ) == null ) {
869+
// not a basic type
870+
query.setTupleTransformer( new NativeQueryConstructorTransformer<>( resultClass ) );
871+
}
872+
else {
873+
query.addResultTypeClass( resultClass );
874+
}
866875
}
867876
}
868877

@@ -884,7 +893,13 @@ public <T> NativeQueryImplementor<T> createNativeQuery(String sqlString, String
884893
@SuppressWarnings("unchecked")
885894
final NativeQueryImplementor<T> query = createNativeQuery( sqlString, resultSetMappingName );
886895
if ( Tuple.class.equals( resultClass ) ) {
887-
query.setTupleTransformer( new NativeQueryTupleTransformer() );
896+
query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE );
897+
}
898+
else if ( Map.class.equals( resultClass ) ) {
899+
query.setTupleTransformer( NativeQueryMapTransformer.INSTANCE );
900+
}
901+
else if ( List.class.equals( resultClass ) ) {
902+
query.setTupleTransformer( NativeQueryListTransformer.INSTANCE );
888903
}
889904
return query;
890905
}

hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,4 +898,11 @@ else if (member instanceof Method) {
898898
throw new AssertionFailure("member should have been a method or field");
899899
}
900900
}
901+
902+
public static boolean isClass(Class<?> resultClass) {
903+
return !resultClass.isArray()
904+
&& !resultClass.isPrimitive()
905+
&& !resultClass.isEnum()
906+
&& !resultClass.isInterface();
907+
}
901908
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.jpa.spi;
8+
9+
import org.hibernate.InstantiationException;
10+
import org.hibernate.query.TupleTransformer;
11+
12+
import java.lang.reflect.Constructor;
13+
import java.util.List;
14+
15+
/**
16+
* A {@link TupleTransformer} for handling {@link List} results from native queries.
17+
*
18+
* @author Gavin King
19+
*/
20+
public class NativeQueryConstructorTransformer<T> implements TupleTransformer<T> {
21+
22+
private final Class<T> resultClass;
23+
private Constructor<T> constructor;
24+
25+
private Constructor<T> constructor(Object[] elements) {
26+
if ( constructor == null ) {
27+
try {
28+
// we cannot be sure of the "true" parameter types
29+
// of the constructor we're looking for, so we need
30+
// to do something a bit weird here: match on just
31+
// the number of parameters
32+
for ( final Constructor<?> candidate : resultClass.getDeclaredConstructors() ) {
33+
final Class<?>[] parameterTypes = candidate.getParameterTypes();
34+
if ( parameterTypes.length == elements.length ) {
35+
// found a candidate with the right number
36+
// of parameters
37+
if ( constructor == null ) {
38+
constructor = resultClass.getDeclaredConstructor( parameterTypes );
39+
constructor.setAccessible( true );
40+
}
41+
else {
42+
// ambiguous, more than one constructor
43+
// with the right number of parameters
44+
constructor = null;
45+
break;
46+
}
47+
}
48+
}
49+
}
50+
catch (Exception e) {
51+
throw new InstantiationException( "Cannot instantiate query result type ", resultClass, e );
52+
}
53+
if ( constructor == null ) {
54+
throw new InstantiationException( "Result class must have a single constructor with exactly "
55+
+ elements.length + " parameters", resultClass );
56+
}
57+
}
58+
return constructor;
59+
}
60+
61+
public NativeQueryConstructorTransformer(Class<T> resultClass) {
62+
this.resultClass = resultClass;
63+
}
64+
65+
@Override
66+
public T transformTuple(Object[] tuple, String[] aliases) {
67+
try {
68+
return constructor( tuple ).newInstance( tuple );
69+
}
70+
catch (Exception e) {
71+
throw new InstantiationException( "Cannot instantiate query result type", resultClass, e );
72+
}
73+
}
74+
}

hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryListTransformer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
* @author Gavin King
1717
*/
1818
public class NativeQueryListTransformer implements TupleTransformer<List<Object>> {
19+
20+
public static final NativeQueryListTransformer INSTANCE = new NativeQueryListTransformer();
21+
1922
@Override
2023
public List<Object> transformTuple(Object[] tuple, String[] aliases) {
2124
return List.of( tuple );

hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryMapTransformer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
* @author Gavin King
2020
*/
2121
public class NativeQueryMapTransformer implements TupleTransformer<Map<String,Object>> {
22+
23+
public static final NativeQueryMapTransformer INSTANCE = new NativeQueryMapTransformer();
24+
2225
@Override
2326
public Map<String,Object> transformTuple(Object[] tuple, String[] aliases) {
2427
Map<String,Object> map = new HashMap<>( aliases.length );

hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
*/
2828
public class NativeQueryTupleTransformer implements ResultTransformer<Tuple>, TypedTupleTransformer<Tuple> {
2929

30+
public static final NativeQueryTupleTransformer INSTANCE = new NativeQueryTupleTransformer();
31+
3032
@Override
3133
public Tuple transformTuple(Object[] tuple, String[] aliases) {
3234
return new NativeTupleImpl( tuple, aliases );

hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import jakarta.persistence.Tuple;
1515
import org.hibernate.AssertionFailure;
16+
import org.hibernate.InstantiationException;
1617
import org.hibernate.ScrollMode;
1718
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
1819
import org.hibernate.engine.jdbc.spi.JdbcServices;
@@ -60,6 +61,7 @@
6061
import org.hibernate.sql.results.spi.ListResultsConsumer;
6162
import org.hibernate.sql.results.spi.RowTransformer;
6263

64+
import static org.hibernate.internal.util.ReflectHelper.isClass;
6365
import static org.hibernate.query.sqm.internal.QuerySqmImpl.CRITERIA_HQL_STRING;
6466

6567
/**
@@ -207,9 +209,12 @@ else if ( resultType == List.class ) {
207209
else if ( Map.class.equals( resultType ) ) {
208210
return (RowTransformer<T>) new RowTransformerMapImpl( tupleMetadata );
209211
}
210-
else {
212+
else if ( isClass( resultType ) ) {
211213
return new RowTransformerConstructorImpl<>( resultType, tupleMetadata );
212214
}
215+
else {
216+
throw new InstantiationException( "Query result type is not instantiable", resultType );
217+
}
213218
}
214219
}
215220
}

hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ public void testRecordInstantiationWithoutAlias(SessionFactoryScope scope) {
5050
);
5151
}
5252

53+
@Test
54+
public void testSqlRecordInstantiationWithoutAlias(SessionFactoryScope scope) {
55+
scope.inTransaction(
56+
session -> {
57+
session.persist(new Thing(1L, "thing"));
58+
Record result = session.createNativeQuery("select id, upper(name) as name from thingy_table", Record.class)
59+
.addSynchronizedEntityClass(Thing.class)
60+
.getSingleResult();
61+
assertEquals( result.id(), 1L );
62+
assertEquals( result.name(), "THING" );
63+
session.getTransaction().setRollbackOnly();
64+
}
65+
);
66+
}
67+
5368
@Test
5469
public void testTupleInstantiationWithAlias(SessionFactoryScope scope) {
5570
scope.inTransaction(
@@ -163,7 +178,7 @@ public void testSqlListInstantiationWithoutAlias(SessionFactoryScope scope) {
163178
scope.inTransaction(
164179
session -> {
165180
session.persist(new Thing(1L, "thing"));
166-
List result = session.createNativeQuery("select id as id, upper(name) as name from thingy_table", List.class)
181+
List result = session.createNativeQuery("select id, upper(name) as name from thingy_table", List.class)
167182
.addSynchronizedEntityClass(Thing.class)
168183
.getSingleResult();
169184
assertEquals( result.get(0), 1L );

0 commit comments

Comments
 (0)