diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 7b3d225e9f..4914beb4d7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -23,8 +23,6 @@ import jakarta.persistence.TupleElement; import jakarta.persistence.TypedQuery; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -34,6 +32,7 @@ import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import org.springframework.beans.BeanUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; @@ -46,6 +45,8 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution; import org.springframework.data.jpa.repository.support.QueryHints; import org.springframework.data.jpa.util.JpaMetamodel; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -53,7 +54,7 @@ import org.springframework.jdbc.support.JdbcUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * Abstract base class to implement {@link RepositoryQuery}s. @@ -288,8 +289,8 @@ protected Class getTypeToRead(ReturnedType returnedType) { return returnedType.isProjecting() && returnedType.getReturnedType().isInterface() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) // - ? Tuple.class // - : null; + ? Tuple.class // + : null; } /** @@ -314,6 +315,10 @@ public static class TupleConverter implements Converter { private final UnaryOperator tupleWrapper; + private final boolean dtoProjection; + + private final @Nullable PreferredConstructor preferredConstructor; + /** * Creates a new {@link TupleConverter} for the given {@link ReturnedType}. * @@ -336,6 +341,14 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) { this.type = type; this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity(); + this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface() + && !type.getInputProperties().isEmpty(); + + if (this.dtoProjection) { + this.preferredConstructor = PreferredConstructorDiscoverer.discover(String.class); + } else { + this.preferredConstructor = null; + } } @Override @@ -356,23 +369,26 @@ public Object convert(Object source) { } } - if(type.isProjecting() && !type.getReturnedType().isInterface() && !type.getInputProperties().isEmpty()) { - List ctorArgs = new ArrayList<>(type.getInputProperties().size()); - type.getInputProperties().forEach(it -> { - ctorArgs.add(tuple.get(it)); - }); - try { - return type.getReturnedType().getConstructor(ctorArgs.stream().map(Object::getClass).toArray(Class[]::new)).newInstance(ctorArgs.toArray()); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } + if (dtoProjection) { + + Object[] ctorArgs = new Object[type.getInputProperties().size()]; + + for (int i = 0; i < type.getInputProperties().size(); i++) { + ctorArgs[i] = tuple.get(i); + } + + try { + + if (preferredConstructor.getParameterCount() == ctorArgs.length) { + return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs); + } + + return BeanUtils.instantiateClass(type.getReturnedType() + .getConstructor(Arrays.stream(ctorArgs).map(Object::getClass).toArray(Class[]::new)), ctorArgs); + } catch (ReflectiveOperationException e) { + ReflectionUtils.handleReflectionException(e); + } + } return new TupleBackedMap(tupleWrapper.apply(tuple)); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 7b89245661..a85af07219 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -69,8 +69,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param valueExpressionDelegate must not be {@literal null}. */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, - ValueExpressionDelegate valueExpressionDelegate) { + @Nullable String countQueryString, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { super(method, em); @@ -99,15 +98,17 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri }); this.queryRewriter = queryRewriter; - ReturnedType returnedType = method.getResultProcessor().getReturnedType(); JpaParameters parameters = method.getParameters(); - if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) { - this.querySortRewriter = new CachingQuerySortRewriter(); - } else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) { - this.querySortRewriter = new ProjectingSortRewriter(); + + if (parameters.hasDynamicProjection()) { + this.querySortRewriter = SimpleQuerySortRewriter.INSTANCE; } else { - this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE; + if (parameters.hasPageableParameter() || parameters.hasSortParameter()) { + this.querySortRewriter = new CachingQuerySortRewriter(); + } else { + this.querySortRewriter = new UnsortedCachingQuerySortRewriter(); + } } Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(), @@ -119,19 +120,13 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { Sort sort = accessor.getSort(); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - - String sortedQueryString = null; - if(querySortRewriter.equals(NoOpQuerySortRewriter.INSTANCE) && accessor.findDynamicProjection() != null && !accessor.findDynamicProjection().isInterface()) { - sortedQueryString = getSortedQueryString(new ProjectingSortRewriter(), query, sort, processor.getReturnedType()); - } else { - sortedQueryString = getSortedQueryString(sort, processor.getReturnedType()); - } - - Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType()); + ReturnedType returnedType = processor.getReturnedType(); + String sortedQueryString = getSortedQueryString(sort, returnedType); + Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType); QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query); - // it is ok to reuse the binding contained in the ParameterBinder although we create a new query String because the + // it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the // parameters in the query do not change. return parameterBinder.get().bindAndPrepare(query, metadata, accessor); } @@ -140,10 +135,6 @@ String getSortedQueryString(Sort sort, ReturnedType returnedType) { return querySortRewriter.getSorted(query, sort, returnedType); } - private static String getSortedQueryString(QuerySortRewriter rewriter, DeclaredQuery query, Sort sort, ReturnedType returnedType) { - return rewriter.getSorted(query, sort, returnedType); - } - @Override protected ParameterBinder createBinder() { return createBinder(query); @@ -223,8 +214,8 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla String applySorting(CachableQuery cachableQuery) { - return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).rewrite(cachableQuery.getSort(), - cachableQuery.getReturnedType()); + return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()) + .rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType())); } /** @@ -237,21 +228,17 @@ interface QuerySortRewriter { /** * No-op query rewriter. */ - enum NoOpQuerySortRewriter implements QuerySortRewriter { + enum SimpleQuerySortRewriter implements QuerySortRewriter { INSTANCE; public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { - if (sort.isSorted()) { - throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); - } - - return query.getQueryString(); + return QueryEnhancerFactory.forQuery(query).rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } } - static class ProjectingSortRewriter implements QuerySortRewriter { + static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter { private volatile String cachedQueryString; @@ -263,7 +250,8 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp String cachedQueryString = this.cachedQueryString; if (cachedQueryString == null) { - this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query).rewrite(sort, returnedType); + this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query) + .rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } return cachedQueryString; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java index 07ef7d15be..8ec778fb70 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java @@ -18,7 +18,6 @@ import java.util.Set; import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; /** @@ -54,8 +53,8 @@ public String applySorting(Sort sort, @Nullable String alias) { } @Override - public String rewrite(Sort sort, ReturnedType returnedType) { - return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); + public String rewrite(QueryRewriteInformation rewriteInformation) { + return QueryUtils.applySorting(this.query.getQueryString(), rewriteInformation.getSort(), alias); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java new file mode 100644 index 0000000000..ee17ca3f04 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryRewriteInformation.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.ReturnedType; + +/** + * Default {@link org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation} implementation. + * + * @author Mark Paluch + */ +record DefaultQueryRewriteInformation(Sort sort, + ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation { + @Override + public Sort getSort() { + return sort(); + } + + @Override + public ReturnedType getReturnedType() { + return returnedType(); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java index 558575f15c..c87a5d63de 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java @@ -29,6 +29,7 @@ * * * @author Mark Paluch + * @since 3.5 */ class DtoProjectionTransformerDelegate { @@ -40,7 +41,8 @@ public DtoProjectionTransformerDelegate(ReturnedType returnedType) { public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { - if (!returnedType.isProjecting() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { + if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() + || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { return selectionList; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index a8a0ef19c8..f9ebe1efa7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -50,7 +50,6 @@ import java.util.StringJoiner; import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -298,17 +297,20 @@ public DeclaredQuery getQuery() { @Override public String applySorting(Sort sort) { - return applySorting(sort, detectAlias()); + return doApplySorting(sort, detectAlias()); } @Override - public String rewrite(Sort sort, ReturnedType returnedType) { - return applySorting(sort, primaryAlias); + public String rewrite(QueryRewriteInformation rewriteInformation) { + return doApplySorting(rewriteInformation.getSort(), primaryAlias); } @Override public String applySorting(Sort sort, @Nullable String alias) { + return doApplySorting(sort, alias); + } + private String doApplySorting(Sort sort, @Nullable String alias) { String queryString = query.getQueryString(); Assert.hasText(queryString, "Query must not be null or empty"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index ad0597983a..f5ed753c97 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -200,8 +200,9 @@ public String applySorting(Sort sort) { } @Override - public String rewrite(Sort sort, ReturnedType returnedType) { - return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), returnedType).visit(context)); + public String rewrite(QueryRewriteInformation rewriteInformation) { + return QueryRenderer.TokenRenderer.render(sortFunction + .apply(rewriteInformation.getSort(), detectAlias(), rewriteInformation.getReturnedType()).visit(context)); } /** @@ -319,6 +320,13 @@ public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarExce } } + /** + * Functional interface to rewrite a query considering {@link Sort} and {@link ReturnedType}. The function returns a + * visitor object that can visit the parsed query tree. + * + * @since 3.5 + */ + @FunctionalInterface interface SortedQueryRewriteFunction { ParseTreeVisitor apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java index ba0dfcf658..0995dd8700 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java @@ -82,11 +82,19 @@ public interface QueryEnhancer { * @param sort the sort specification to apply. * @param alias the alias to be used in the order by clause. May be {@literal null} or empty. * @return the modified query string. + * @deprecated since 3.5, use {@link #rewrite(Sort, ReturnedType)} instead. */ - @Deprecated + @Deprecated(since = "3.5", forRemoval = true) String applySorting(Sort sort, @Nullable String alias); - String rewrite(Sort sort, ReturnedType returnedType); + /** + * Rewrite the query to include sorting and apply {@link ReturnedType} customizations. + * + * @param rewriteInformation the rewrite information to apply. + * @return the modified query string. + * @since 3.5 + */ + String rewrite(QueryRewriteInformation rewriteInformation); /** * Creates a count projected query from the given original query. @@ -105,4 +113,22 @@ default String createCountQueryFor() { */ String createCountQueryFor(@Nullable String countProjection); + /** + * Interface to describe the information needed to rewrite a query. + * + * @since 3.5 + */ + interface QueryRewriteInformation { + + /** + * @return the sort specification to apply. + */ + Sort getSort(); + + /** + * @return type expected to be returned by the query. + */ + ReturnedType getReturnedType(); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java index 2e9a1fbb22..4302c63650 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java @@ -83,17 +83,16 @@ class FetchableFluentQueryByPredicate extends FluentQuerySupport imp BiFunction> pagedFinder, Function countOperation, Function existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) { this(entityPath, predicate, entityInformation, (Class) entityInformation.getJavaType(), Sort.unsorted(), 0, - Collections.emptySet(), finder, scrollQueryFactory, - pagedFinder, countOperation, existsOperation, entityManager, projectionFactory); + Collections.emptySet(), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager, + projectionFactory); } private FetchableFluentQueryByPredicate(EntityPath entityPath, Predicate predicate, JpaEntityInformation entityInformation, Class resultType, Sort sort, int limit, Collection properties, Function> finder, ScrollQueryFactory> scrollQueryFactory, - BiFunction> pagedFinder, - Function countOperation, Function existsOperation, - EntityManager entityManager, ProjectionFactory projectionFactory) { + BiFunction> pagedFinder, Function countOperation, + Function existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) { super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory); this.entityInformation = entityInformation; @@ -142,8 +141,7 @@ public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit, mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, - entityManager, - projectionFactory); + entityManager, projectionFactory); } @Override @@ -296,6 +294,9 @@ public Window scroll(ReturnedType returnedType, Sort sort, int limit, ScrollP } } + /** + * @since 3.5 + */ private static class DtoProjection extends ExpressionBase { private final Expression[] projection; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java index cf6c29fdea..1f1a18ac44 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.List; +import org.springframework.lang.Nullable; + import com.querydsl.core.types.Expression; import com.querydsl.core.types.ExpressionBase; import com.querydsl.core.types.ExpressionUtils; @@ -30,6 +32,15 @@ import com.querydsl.core.types.Visitor; import com.querydsl.jpa.JPQLSerializer; +/** + * Expression based on a {@link Tuple}. It's a simplified variant of {@link com.querydsl.core.types.QTuple} without + * being a {@link com.querydsl.core.types.FactoryExpressionBase} as we do not want Querydsl to instantiate any tuples. + * JPA is doing that for us. + * + * @author Mark Paluch + * @since 3.5 + */ +@SuppressWarnings("unchecked") class JakartaTuple extends ExpressionBase { private final List> args; @@ -61,7 +72,8 @@ protected JakartaTuple(List> args) { } @Override - public R accept(Visitor v, C context) { + @Nullable + public R accept(Visitor v, @Nullable C context) { if (v instanceof JPQLSerializer) { return Projections.tuple(args).accept(v, context); @@ -74,8 +86,7 @@ public R accept(Visitor v, C context) { public boolean equals(Object obj) { if (obj == this) { return true; - } else if (obj instanceof FactoryExpression) { - FactoryExpression c = (FactoryExpression) obj; + } else if (obj instanceof FactoryExpression c) { return args.equals(c.getArgs()) && getType().equals(c.getType()); } else { return false; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java index b3faa0fb5c..4e8ef371d2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java @@ -25,6 +25,7 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.querydsl.QSort; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; @@ -37,6 +38,7 @@ import com.querydsl.jpa.EclipseLinkTemplates; import com.querydsl.jpa.HQLTemplates; import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.AbstractJPAQuery; import com.querydsl.jpa.impl.JPAQuery; @@ -77,11 +79,25 @@ public Querydsl(EntityManager em, PathBuilder builder) { */ public AbstractJPAQuery> createQuery() { - return switch (provider) { - case ECLIPSELINK -> new SpringDataJpaQuery<>(em, EclipseLinkTemplates.DEFAULT); - case HIBERNATE -> new SpringDataJpaQuery<>(em, HQLTemplates.DEFAULT); - default -> new SpringDataJpaQuery<>(em); - }; + JPQLTemplates templates = getTemplates(); + return templates != null ? new SpringDataJpaQuery<>(em, templates) : new SpringDataJpaQuery<>(em); + } + + /** + * Obtains the {@link JPQLTemplates} for the configured {@link EntityManager}. Can return {@literal null} to use the + * default templates. + * + * @return the {@link JPQLTemplates} for the configured {@link EntityManager} or {@literal null} to use the default. + * @since 3.5 + */ + @Nullable + public JPQLTemplates getTemplates() { + + return switch (provider) { + case ECLIPSELINK -> EclipseLinkTemplates.DEFAULT; + case HIBERNATE -> HQLTemplates.DEFAULT; + default -> JPQLTemplates.DEFAULT; + }; } /** @@ -198,11 +214,11 @@ private NullHandling toQueryDslNullHandling(org.springframework.data.domain.Sort Assert.notNull(nullHandling, "NullHandling must not be null"); - return switch (nullHandling) { - case NULLS_FIRST -> NullHandling.NullsFirst; - case NULLS_LAST -> NullHandling.NullsLast; - default -> NullHandling.Default; - }; + return switch (nullHandling) { + case NULLS_FIRST -> NullHandling.NullsFirst; + case NULLS_LAST -> NullHandling.NullsLast; + default -> NullHandling.Default; + }; } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java index 5d747dea4c..5eba7dd36d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java @@ -32,7 +32,11 @@ import com.querydsl.jpa.impl.JPAUtil; /** + * Customized String-Query implementation that specifically routes tuple query creation to + * {@code EntityManager#createQuery(queryString, Tuple.class)}. + * * @author Mark Paluch + * @since 3.5 */ class SpringDataJpaQuery extends JPAQuery { diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index 8ac41c7d51..95366d30aa 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -69,7 +69,7 @@ This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM U [WARNING] ==== JPQL constructor expressions must not contain aliases for selected columns. -While `SELECT u as user, count(u.roles) as roleCount FROM USER u ...` is a valid usecase for interface based projections that rely on column names from the returned `Tuple`, the same construct is invalid when requesting a DTO where it needs to be `SELECT u, count(u.roles) FROM USER u ...`. + +While `SELECT u as user, count(u.roles) as roleCount FROM USER u …` is a valid query for interface-based projections that rely on column names from the returned `Tuple`, the same construct is invalid when requesting a DTO where it needs to be `SELECT u, count(u.roles) FROM USER u …`. + Some persistence providers may be lenient about this, others not. ====