HHH-16710 constructor-based instantiation for native queries

This commit is contained in:
Gavin 2023-05-29 15:52:17 +02:00 committed by Gavin King
parent 280df7c98d
commit acf9495af3
8 changed files with 131 additions and 7 deletions

View File

@ -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 <T> void addResultType(Class<T> resultClass, NativeQueryImplementor<T> 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<T> 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;
}

View File

@ -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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<T> implements TupleTransformer<T> {
private final Class<T> resultClass;
private Constructor<T> constructor;
private Constructor<T> 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<T> 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 );
}
}
}

View File

@ -16,6 +16,9 @@ import java.util.List;
* @author Gavin King
*/
public class NativeQueryListTransformer implements TupleTransformer<List<Object>> {
public static final NativeQueryListTransformer INSTANCE = new NativeQueryListTransformer();
@Override
public List<Object> transformTuple(Object[] tuple, String[] aliases) {
return List.of( tuple );

View File

@ -19,6 +19,9 @@ import static java.util.Locale.ROOT;
* @author Gavin King
*/
public class NativeQueryMapTransformer implements TupleTransformer<Map<String,Object>> {
public static final NativeQueryMapTransformer INSTANCE = new NativeQueryMapTransformer();
@Override
public Map<String,Object> transformTuple(Object[] tuple, String[] aliases) {
Map<String,Object> map = new HashMap<>( aliases.length );

View File

@ -27,6 +27,8 @@ import static java.util.Locale.ROOT;
*/
public class NativeQueryTupleTransformer implements ResultTransformer<Tuple>, TypedTupleTransformer<Tuple> {
public static final NativeQueryTupleTransformer INSTANCE = new NativeQueryTupleTransformer();
@Override
public Tuple transformTuple(Object[] tuple, String[] aliases) {
return new NativeTupleImpl( tuple, aliases );

View File

@ -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<R> implements SelectQueryPlan<R> {
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerMapImpl( tupleMetadata );
}
else {
else if ( isClass( resultType ) ) {
return new RowTransformerConstructorImpl<>( resultType, tupleMetadata );
}
else {
throw new InstantiationException( "Query result type is not instantiable", resultType );
}
}
}
}

View File

@ -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 );