diff --git a/documentation/src/main/asciidoc/introduction/Querying.adoc b/documentation/src/main/asciidoc/introduction/Querying.adoc index 3b0473a2602e..73f6ad47356b 100644 --- a/documentation/src/main/asciidoc/introduction/Querying.adoc +++ b/documentation/src/main/asciidoc/introduction/Querying.adoc @@ -660,7 +660,18 @@ for (var result : results) { ---- Notice that we're able to declare the `record` right before the line which executes the query. -Now, this is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better. +This works just as well with queries written in SQL: + +[source,java] +---- +record BookInfo(String isbn, String title, int pages) {} + +List resultList = + session.createNativeQuery("select title, isbn, pages from Book", BookInfo.class) + .getResultList(); +---- + +Now, this approach is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better. But perhaps you find it more aesthetically pleasing. And if we're going to be passing query results around the system, the use of a `record` type is _much_ better. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 51ebd4aff58f..935d4f4e3ccd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -356,6 +356,15 @@ protected void setTupleTransformerForResultType(Class resultClass) { } } + /** + * If the result type of the query is {@link Tuple}, {@link Map}, {@link List}, + * or any record or class type with an appropriate constructor which is NOT a + * registered basic type, then we attempt to repackage the result tuple as an + * instance of the result type using an appropriate {@link TupleTransformer}. + * + * @param resultClass The requested result type of the query + * @return A {@link TupleTransformer} responsible for repackaging the result type + */ protected @Nullable TupleTransformer determineTupleTransformerForResultType(Class resultClass) { if ( Tuple.class.equals( resultClass ) ) { return NativeQueryTupleTransformer.INSTANCE; @@ -369,7 +378,8 @@ else if ( List.class.equals( resultClass ) ) { else if ( resultClass != Object.class && resultClass != Object[].class ) { // TODO: this is extremely fragile and probably a bug if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { - // not a basic type + // not a basic type, so something we can attempt + // to instantiate to repackage the results return new NativeQueryConstructorTransformer<>( resultClass ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java index ade12c7fb8b4..6b1c12e3c2a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java @@ -17,6 +17,7 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.ScrollableResultsImplementor; +import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sql.spi.NativeSelectQueryPlan; import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; @@ -31,6 +32,10 @@ import static java.util.Collections.emptyList; /** + * Standard implementation of {@link SelectQueryPlan} for + * {@link org.hibernate.query.NativeQuery}, that is, for + * queries written in SQL. + * * @author Steve Ebersole */ public class NativeSelectQueryPlanImpl implements NativeSelectQueryPlan { 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 7f9f1ea443e5..b32143dfc179 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 @@ -19,7 +19,6 @@ import org.hibernate.engine.spi.SubselectFetch; import org.hibernate.internal.EmptyScrollableResults; import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.query.Query; import org.hibernate.query.QueryTypeMismatchException; import org.hibernate.query.TupleTransformer; import org.hibernate.query.spi.DomainQueryExecutionContext; @@ -66,9 +65,9 @@ import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; /** - * Standard Hibernate implementation of SelectQueryPlan for SQM-backed - * {@link Query} implementations, which means - * HQL/JPQL or {@link jakarta.persistence.criteria.CriteriaQuery} + * Standard implementation of {@link SelectQueryPlan} for SQM-backed + * implementations of {@link org.hibernate.query.Query}, that is, for + * HQL/JPQL or for {@link jakarta.persistence.criteria.CriteriaQuery}. * * @author Steve Ebersole */ @@ -247,7 +246,15 @@ private static List> selections(SqmSelectStatement sqm) { char.class, Character.class ); - @SuppressWarnings("unchecked") + /** + * If the result type of the query is {@link Tuple}, {@link Map}, {@link List}, + * or any record or class type with an appropriate constructor, then we attempt + * to repackage the result tuple as an instance of the result type using an + * appropriate {@link RowTransformer}. + * + * @param resultClass The requested result type of the query + * @return A {@link RowTransformer} responsible for repackaging the result type + */ protected static RowTransformer determineRowTransformer( SqmSelectStatement sqm, Class resultClass, @@ -260,73 +267,90 @@ else if ( resultClass == null || resultClass == Object.class ) { return RowTransformerStandardImpl.instance(); } else { - final Class resultType = (Class) - WRAPPERS.getOrDefault( resultClass, resultClass ); - final List> selections = selections( sqm ); + final var selections = selections( sqm ); if ( selections == null ) { - throw new AssertionFailure("No selections"); + throw new AssertionFailure( "No selections" ); } - switch ( selections.size() ) { - case 0: - throw new AssertionFailure("No selections"); - case 1: - final SqmSelection selection = selections.get(0); - if ( isSelectionAssignableToResultType( selection, resultType ) ) { - return RowTransformerSingularReturnImpl.instance(); - } - else if ( resultType.isArray() ) { - return (RowTransformer) RowTransformerArrayImpl.instance(); - } - else if ( List.class.equals( resultType ) ) { - return (RowTransformer) RowTransformerListImpl.instance(); - } - else if ( Tuple.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); - } - else if ( Map.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); - } - else if ( isClass( resultType ) ) { - try { - return new RowTransformerConstructorImpl<>( - resultType, - tupleMetadata, - sqm.nodeBuilder().getTypeConfiguration() - ); - } - catch (InstantiationException ie) { - return new RowTransformerCheckingImpl<>( resultType ); - } - } - else { - return new RowTransformerCheckingImpl<>( resultType ); - } - default: - if ( resultType.isArray() ) { - return (RowTransformer) RowTransformerArrayImpl.instance(); - } - else if ( List.class.equals( resultType ) ) { - return (RowTransformer) RowTransformerListImpl.instance(); - } - else if ( Tuple.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); - } - else if ( Map.class.equals( resultType ) ) { - return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); - } - else if ( isClass( resultType ) ) { - return new RowTransformerConstructorImpl<>( - resultType, - tupleMetadata, - sqm.nodeBuilder().getTypeConfiguration() - ); - } - else { - throw new QueryTypeMismatchException( "Result type '" + resultType.getSimpleName() - + "' cannot be used to package the selected expressions" ); - } + else { + final Class resultType = primitiveToWrapper( resultClass ); + return switch ( selections.size() ) { + case 0 -> throw new AssertionFailure( "No selections" ); + case 1 -> singleItemRowTransformer( sqm, tupleMetadata, selections.get( 0 ), resultType ); + default -> multipleItemRowTransformer( sqm, tupleMetadata, resultType ); + }; + } + } + } + + /** + * We tolerate the use of primitive query result types, for example, + * {@code long.class} instead of {@code Long.class}. Note that this + * has no semantics: we don't attempt to enforce that the query + * result is non-null if it is primitive. + */ + @SuppressWarnings("unchecked") + private static Class primitiveToWrapper(Class resultClass) { + // this cast, which looks like complete nonsense, is perfectly correct, + // since Java assigns the type Class to te expression long.class + // even though the resulting class object is distinct from Long.class + return (Class) WRAPPERS.getOrDefault( resultClass, resultClass ); + } + + @SuppressWarnings("unchecked") + private static RowTransformer multipleItemRowTransformer + (SqmSelectStatement sqm, TupleMetadata tupleMetadata, Class resultType) { + if ( resultType.isArray() ) { + return (RowTransformer) RowTransformerArrayImpl.instance(); + } + else if ( List.class.equals( resultType ) ) { + return (RowTransformer) RowTransformerListImpl.instance(); + } + else if ( Tuple.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); + } + else if ( Map.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + } + else if ( isClass( resultType ) ) { + return new RowTransformerConstructorImpl<>( resultType, tupleMetadata, + sqm.nodeBuilder().getTypeConfiguration() ); + } + else { + throw new QueryTypeMismatchException( "Result type '" + resultType.getSimpleName() + + "' cannot be used to package the selected expressions" ); + } + } + + @SuppressWarnings("unchecked") + private static RowTransformer singleItemRowTransformer + (SqmSelectStatement sqm, TupleMetadata tupleMetadata, SqmSelection selection, Class resultType) { + if ( isSelectionAssignableToResultType( selection, resultType ) ) { + return RowTransformerSingularReturnImpl.instance(); + } + else if ( resultType.isArray() ) { + return (RowTransformer) RowTransformerArrayImpl.instance(); + } + else if ( List.class.equals( resultType ) ) { + return (RowTransformer) RowTransformerListImpl.instance(); + } + else if ( Tuple.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); + } + else if ( Map.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + } + else if ( isClass( resultType ) ) { + try { + return new RowTransformerConstructorImpl<>( resultType, tupleMetadata, + sqm.nodeBuilder().getTypeConfiguration() ); + } + catch (InstantiationException ie) { + return new RowTransformerCheckingImpl<>( resultType ); } } + else { + return new RowTransformerCheckingImpl<>( resultType ); + } } private static RowTransformer makeRowTransformerTupleTransformerAdapter( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index ddb2191d278e..668954a76821 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -1222,6 +1222,14 @@ private static void verifySelectionType( } } + /** + * Any query result can be represented as a {@link Tuple}, {@link List}, or {@link Map}, + * simply by repackaging the result tuple. Also, any query result is assignable to + * {@code Object}, or can be returned as an instance of {@code Object[]}. + * + * @see ConcreteSqmSelectQueryPlan#determineRowTransformer + * @see org.hibernate.query.sql.internal.NativeQueryImpl#determineTupleTransformerForResultType + */ public static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { return expectedResultClass == null || expectedResultClass == Object.class 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 173d4775820a..9471937f5c38 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 @@ -55,7 +55,7 @@ public void testRecordInstantiationWithoutAlias(SessionFactoryScope scope) { } @Test - public void testSqlRecordInstantiationWithoutAlias(SessionFactoryScope scope) { + public void testSqlRecordInstantiationWithoutMapping(SessionFactoryScope scope) { scope.inTransaction( session -> { session.persist(new Thing(1L, "thing")); @@ -69,6 +69,23 @@ public void testSqlRecordInstantiationWithoutAlias(SessionFactoryScope scope) { ); } + @Test + public void testSqlRecordInstantiationWithMapping(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Record result = (Record) session.createNativeQuery( "select id, upper(name) as name from thingy_table", Record.class) + .addScalar("id", Long.class) + .addScalar("name", String.class) + .addSynchronizedEntityClass(Thing.class) + .getSingleResult(); + assertEquals( result.id(), 1L ); + assertEquals( result.name(), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + @Test public void testTupleInstantiationWithAlias(SessionFactoryScope scope) { scope.inTransaction(