Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion documentation/src/main/asciidoc/introduction/Querying.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<BookInfo> 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,15 @@ protected <T> void setTupleTransformerForResultType(Class<T> 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;
Expand All @@ -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 );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<R> implements NativeSelectQueryPlan<R> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -247,7 +246,15 @@ private static List<SqmSelection<?>> 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 <T> RowTransformer<T> determineRowTransformer(
SqmSelectStatement<?> sqm,
Class<T> resultClass,
Expand All @@ -260,73 +267,90 @@ else if ( resultClass == null || resultClass == Object.class ) {
return RowTransformerStandardImpl.instance();
}
else {
final Class<T> resultType = (Class<T>)
WRAPPERS.getOrDefault( resultClass, resultClass );
final List<SqmSelection<?>> 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<T>) RowTransformerArrayImpl.instance();
}
else if ( List.class.equals( resultType ) ) {
return (RowTransformer<T>) RowTransformerListImpl.instance();
}
else if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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<T>) RowTransformerArrayImpl.instance();
}
else if ( List.class.equals( resultType ) ) {
return (RowTransformer<T>) RowTransformerListImpl.instance();
}
else if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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<T> 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 <T> Class<T> primitiveToWrapper(Class<T> resultClass) {
// this cast, which looks like complete nonsense, is perfectly correct,
// since Java assigns the type Class<Long> to te expression long.class
// even though the resulting class object is distinct from Long.class
return (Class<T>) WRAPPERS.getOrDefault( resultClass, resultClass );
}

@SuppressWarnings("unchecked")
private static <T> RowTransformer<T> multipleItemRowTransformer
(SqmSelectStatement<?> sqm, TupleMetadata tupleMetadata, Class<T> resultType) {
if ( resultType.isArray() ) {
return (RowTransformer<T>) RowTransformerArrayImpl.instance();
}
else if ( List.class.equals( resultType ) ) {
return (RowTransformer<T>) RowTransformerListImpl.instance();
}
else if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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 <T> RowTransformer<T> singleItemRowTransformer
(SqmSelectStatement<?> sqm, TupleMetadata tupleMetadata, SqmSelection<?> selection, Class<T> resultType) {
if ( isSelectionAssignableToResultType( selection, resultType ) ) {
return RowTransformerSingularReturnImpl.instance();
}
else if ( resultType.isArray() ) {
return (RowTransformer<T>) RowTransformerArrayImpl.instance();
}
else if ( List.class.equals( resultType ) ) {
return (RowTransformer<T>) RowTransformerListImpl.instance();
}
else if ( Tuple.class.equals( resultType ) ) {
return (RowTransformer<T>) new RowTransformerJpaTupleImpl( tupleMetadata );
}
else if ( Map.class.equals( resultType ) ) {
return (RowTransformer<T>) 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 <T> RowTransformer<T> makeRowTransformerTupleTransformerAdapter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,14 @@ private static <T> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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(
Expand Down