1717import java .util .function .Consumer ;
1818import java .util .function .Supplier ;
1919
20+ import org .checkerframework .checker .nullness .qual .Nullable ;
2021import org .hibernate .CacheMode ;
2122import org .hibernate .FlushMode ;
23+ import org .hibernate .jpa .spi .NativeQueryConstructorTransformer ;
24+ import org .hibernate .jpa .spi .NativeQueryListTransformer ;
25+ import org .hibernate .jpa .spi .NativeQueryMapTransformer ;
2226import org .hibernate .metamodel .spi .MappingMetamodelImplementor ;
2327import org .hibernate .query .QueryFlushMode ;
2428import org .hibernate .HibernateException ;
105109import jakarta .persistence .TypedQuery ;
106110import jakarta .persistence .metamodel .SingularAttribute ;
107111import org .hibernate .type .BasicTypeRegistry ;
112+ import org .hibernate .type .descriptor .java .JavaType ;
113+ import org .hibernate .type .descriptor .java .spi .UnknownBasicJavaType ;
108114import org .hibernate .type .spi .TypeConfiguration ;
109115
110116import static java .lang .Character .isWhitespace ;
111117import static java .util .Collections .addAll ;
118+ import static org .hibernate .internal .util .ReflectHelper .isClass ;
112119import static org .hibernate .internal .util .StringHelper .unqualify ;
113120import static org .hibernate .internal .util .collections .CollectionHelper .isEmpty ;
114121import static org .hibernate .internal .util .collections .CollectionHelper .isNotEmpty ;
@@ -129,6 +136,7 @@ public class NativeQueryImpl<R>
129136 private final List <ParameterOccurrence > parameterOccurrences ;
130137 private final QueryParameterBindings parameterBindings ;
131138
139+ private final Class <R > resultType ;
132140 private final ResultSetMapping resultSetMapping ;
133141 private final boolean resultMappingSuppliedToCtor ;
134142
@@ -166,6 +174,7 @@ public NativeQueryImpl(
166174 return false ;
167175 }
168176 },
177+ null ,
169178 session
170179 );
171180 }
@@ -218,26 +227,9 @@ public NativeQueryImpl(
218227 return false ;
219228 }
220229 },
230+ resultJavaType ,
221231 session
222232 );
223-
224- if ( resultJavaType == Tuple .class ) {
225- setTupleTransformer ( new NativeQueryTupleTransformer () );
226- }
227- else if ( resultJavaType != null && !resultJavaType .isArray () ) {
228- switch ( resultSetMapping .getNumberOfResultBuilders () ) {
229- case 0 :
230- throw new IllegalArgumentException ( "Named query exists, but did not specify a resultClass" );
231- case 1 :
232- final Class <?> actualResultJavaType = resultSetMapping .getResultBuilders ().get ( 0 ).getJavaType ();
233- if ( actualResultJavaType != null && !resultJavaType .isAssignableFrom ( actualResultJavaType ) ) {
234- throw buildIncompatibleException ( resultJavaType , actualResultJavaType );
235- }
236- break ;
237- default :
238- throw new IllegalArgumentException ( "Cannot create TypedQuery for query with more than one return" );
239- }
240- }
241233 }
242234
243235 /**
@@ -258,6 +250,7 @@ public NativeQueryImpl(
258250 mappingMemento .resolve ( resultSetMapping , querySpaceConsumer , context );
259251 return true ;
260252 },
253+ null ,
261254 session
262255 );
263256
@@ -268,6 +261,15 @@ public NativeQueryImpl(
268261 Supplier <ResultSetMapping > resultSetMappingCreator ,
269262 ResultSetMappingHandler resultSetMappingHandler ,
270263 SharedSessionContractImplementor session ) {
264+ this ( memento , resultSetMappingCreator , resultSetMappingHandler , null , session );
265+ }
266+
267+ public NativeQueryImpl (
268+ NamedNativeQueryMemento <?> memento ,
269+ Supplier <ResultSetMapping > resultSetMappingCreator ,
270+ ResultSetMappingHandler resultSetMappingHandler ,
271+ @ Nullable Class <R > resultType ,
272+ SharedSessionContractImplementor session ) {
271273 super ( session );
272274
273275 this .originalSqlString = memento .getOriginalSqlString ();
@@ -279,13 +281,33 @@ public NativeQueryImpl(
279281 this .parameterMetadata = parameterInterpretation .toParameterMetadata ( session );
280282 this .parameterOccurrences = parameterInterpretation .getOrderedParameterOccurrences ();
281283 this .parameterBindings = parameterMetadata .createBindings ( session .getFactory () );
284+ this .resultType = resultType ;
282285 this .querySpaces = new HashSet <>();
283286
284287 this .resultSetMapping = resultSetMappingCreator .get ();
285288
286289 this .resultMappingSuppliedToCtor =
287290 resultSetMappingHandler .resolveResultSetMapping ( resultSetMapping , querySpaces ::add , this );
288291
292+ if ( resultType != null ) {
293+ if ( !resultType .isArray () ) {
294+ switch ( resultSetMapping .getNumberOfResultBuilders () ) {
295+ case 0 :
296+ throw new IllegalArgumentException ( "Named query exists, but did not specify a resultClass" );
297+ case 1 :
298+ final Class <?> actualResultJavaType = resultSetMapping .getResultBuilders ().get ( 0 )
299+ .getJavaType ();
300+ if ( actualResultJavaType != null && !resultType .isAssignableFrom ( actualResultJavaType ) ) {
301+ throw buildIncompatibleException ( resultType , actualResultJavaType );
302+ }
303+ break ;
304+ default :
305+ throw new IllegalArgumentException (
306+ "Cannot create TypedQuery for query with more than one return" );
307+ }
308+ }
309+ setTupleTransformerForResultType ( resultType );
310+ }
289311 applyOptions ( memento );
290312 }
291313
@@ -301,6 +323,7 @@ public NativeQueryImpl(
301323 this .parameterMetadata = parameterInterpretation .toParameterMetadata ( session );
302324 this .parameterOccurrences = parameterInterpretation .getOrderedParameterOccurrences ();
303325 this .parameterBindings = parameterMetadata .createBindings ( session .getFactory () );
326+ this .resultType = null ;
304327 this .querySpaces = new HashSet <>();
305328
306329 this .resultSetMapping = buildResultSetMapping ( resultSetMappingMemento .getName (), false , session );
@@ -310,6 +333,10 @@ public NativeQueryImpl(
310333 }
311334
312335 public NativeQueryImpl (String sqlString , SharedSessionContractImplementor session ) {
336+ this ( sqlString , null , session );
337+ }
338+
339+ public NativeQueryImpl (String sqlString , @ Nullable Class <R > resultType , SharedSessionContractImplementor session ) {
313340 super ( session );
314341
315342 this .querySpaces = new HashSet <>();
@@ -320,11 +347,46 @@ public NativeQueryImpl(String sqlString, SharedSessionContractImplementor sessio
320347 this .parameterMetadata = parameterInterpretation .toParameterMetadata ( session );
321348 this .parameterOccurrences = parameterInterpretation .getOrderedParameterOccurrences ();
322349 this .parameterBindings = parameterMetadata .createBindings ( session .getFactory () );
350+ this .resultType = resultType ;
351+ if ( resultType != null ) {
352+ setTupleTransformerForResultType ( resultType );
353+ }
323354
324355 this .resultSetMapping = resolveResultSetMapping ( sqlString , true , session .getFactory () );
325356 this .resultMappingSuppliedToCtor = false ;
326357 }
327358
359+ protected <T > void setTupleTransformerForResultType (Class <T > resultClass ) {
360+ final TupleTransformer <?> tupleTransformer = determineTupleTransformerForResultType ( resultClass );
361+ if ( tupleTransformer != null ) {
362+ setTupleTransformer ( tupleTransformer );
363+ }
364+ }
365+
366+ protected @ Nullable TupleTransformer <?> determineTupleTransformerForResultType (Class <?> resultClass ) {
367+ if ( Tuple .class .equals ( resultClass ) ) {
368+ return NativeQueryTupleTransformer .INSTANCE ;
369+ }
370+ else if ( Map .class .equals ( resultClass ) ) {
371+ return NativeQueryMapTransformer .INSTANCE ;
372+ }
373+ else if ( List .class .equals ( resultClass ) ) {
374+ return NativeQueryListTransformer .INSTANCE ;
375+ }
376+ else if ( resultClass != Object .class && resultClass != Object [].class ) {
377+ if ( isClass ( resultClass ) && !hasJavaTypeDescriptor ( resultClass ) ) {
378+ // not a basic type
379+ return new NativeQueryConstructorTransformer <>( resultClass );
380+ }
381+ }
382+ return null ;
383+ }
384+
385+ private <T > boolean hasJavaTypeDescriptor (Class <T > resultClass ) {
386+ final JavaType <Object > descriptor = getTypeConfiguration ().getJavaTypeRegistry ().findDescriptor ( resultClass );
387+ return descriptor != null && descriptor .getClass () != UnknownBasicJavaType .class ;
388+ }
389+
328390 @ FunctionalInterface
329391 private interface ResultSetMappingHandler {
330392 boolean resolveResultSetMapping (
@@ -436,11 +498,16 @@ public QueryParameterBindings getParameterBindings() {
436498 return getQueryParameterBindings ();
437499 }
438500
501+ @ Override
502+ public Class <R > getResultType () {
503+ return resultType ;
504+ }
505+
439506 @ Override
440507 public NamedNativeQueryMemento <?> toMemento (String name ) {
441508 return new NamedNativeQueryMementoImpl <>(
442509 name ,
443- extractResultClass ( resultSetMapping ),
510+ resultType != null ? resultType : extractResultClass ( resultSetMapping ),
444511 sqlString ,
445512 originalSqlString ,
446513 resultSetMapping .getMappingIdentifier (),
@@ -459,14 +526,14 @@ public NamedNativeQueryMemento<?> toMemento(String name) {
459526 );
460527 }
461528
462- private Class <? > extractResultClass (ResultSetMapping resultSetMapping ) {
529+ private Class <R > extractResultClass (ResultSetMapping resultSetMapping ) {
463530 final List <ResultBuilder > resultBuilders = resultSetMapping .getResultBuilders ();
464531 if ( resultBuilders .size () == 1 ) {
465532 final ResultBuilder resultBuilder = resultBuilders .get ( 0 );
466533 if ( resultBuilder instanceof ImplicitResultClassBuilder
467534 || resultBuilder instanceof ImplicitModelPartResultBuilderEntity
468535 || resultBuilder instanceof DynamicResultBuilderEntityCalculated ) {
469- return resultBuilder .getJavaType ();
536+ return ( Class < R >) resultBuilder .getJavaType ();
470537 }
471538 }
472539 return null ;
@@ -618,13 +685,29 @@ public KeyedResultList<R> getKeyedResultList(KeyedPage<R> page) {
618685 }
619686
620687 protected SelectQueryPlan <R > resolveSelectQueryPlan () {
688+ final ResultSetMapping mapping ;
689+ if ( resultType != null && resultSetMapping .isDynamic () && resultSetMapping .getNumberOfResultBuilders () == 0 ) {
690+ mapping = ResultSetMapping .resolveResultSetMapping ( originalSqlString , true , getSessionFactory () );
691+
692+ if ( getSessionFactory ().getMappingMetamodel ().isEntityClass ( resultType ) ) {
693+ mapping .addResultBuilder (
694+ Builders .entityCalculated ( unqualify ( resultType .getName () ), resultType .getName (),
695+ LockMode .READ , getSessionFactory () ) );
696+ }
697+ else if ( resultType != Object .class && resultType != Object [].class
698+ && determineTupleTransformerForResultType ( resultType ) == null ) {
699+ mapping .addResultBuilder ( Builders .resultClassBuilder ( resultType , getSessionFactory () ) );
700+ }
701+ }
702+ else {
703+ mapping = resultSetMapping ;
704+ }
621705 return isCacheableQuery ()
622- ? getInterpretationCache ()
623- .resolveSelectQueryPlan ( selectInterpretationsKey (), this ::createQueryPlan )
624- : createQueryPlan ();
706+ ? getInterpretationCache ().resolveSelectQueryPlan ( selectInterpretationsKey (), () -> createQueryPlan ( mapping ) )
707+ : createQueryPlan ( mapping );
625708 }
626709
627- private NativeSelectQueryPlan <R > createQueryPlan () {
710+ private NativeSelectQueryPlan <R > createQueryPlan (ResultSetMapping resultSetMapping ) {
628711 final NativeSelectQueryDefinition <R > queryDefinition = new NativeSelectQueryDefinition <>() {
629712 final String sqlString = expandParameterLists ();
630713
0 commit comments