Skip to content

Commit 5eb527e

Browse files
committed
Polishing.
Simplify Querydsl templates retrieva and String query caching. Update documentation. Skip selection list rewriting if the returned type is an interface.
1 parent 6017988 commit 5eb527e

File tree

10 files changed

+100
-63
lines changed

10 files changed

+100
-63
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

+38-22
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import jakarta.persistence.TupleElement;
2424
import jakarta.persistence.TypedQuery;
2525

26-
import java.lang.reflect.InvocationTargetException;
27-
import java.util.ArrayList;
2826
import java.util.Arrays;
2927
import java.util.Collection;
3028
import java.util.HashMap;
@@ -34,6 +32,7 @@
3432
import java.util.function.UnaryOperator;
3533
import java.util.stream.Collectors;
3634

35+
import org.springframework.beans.BeanUtils;
3736
import org.springframework.core.convert.converter.Converter;
3837
import org.springframework.data.jpa.provider.PersistenceProvider;
3938
import org.springframework.data.jpa.repository.EntityGraph;
@@ -46,14 +45,16 @@
4645
import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution;
4746
import org.springframework.data.jpa.repository.support.QueryHints;
4847
import org.springframework.data.jpa.util.JpaMetamodel;
48+
import org.springframework.data.mapping.PreferredConstructor;
49+
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
4950
import org.springframework.data.repository.query.RepositoryQuery;
5051
import org.springframework.data.repository.query.ResultProcessor;
5152
import org.springframework.data.repository.query.ReturnedType;
5253
import org.springframework.data.util.Lazy;
5354
import org.springframework.jdbc.support.JdbcUtils;
5455
import org.springframework.lang.Nullable;
5556
import org.springframework.util.Assert;
56-
import org.springframework.util.ClassUtils;
57+
import org.springframework.util.ReflectionUtils;
5758

5859
/**
5960
* Abstract base class to implement {@link RepositoryQuery}s.
@@ -288,8 +289,8 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
288289

289290
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface()
290291
&& !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
291-
? Tuple.class //
292-
: null;
292+
? Tuple.class //
293+
: null;
293294
}
294295

295296
/**
@@ -314,6 +315,10 @@ public static class TupleConverter implements Converter<Object, Object> {
314315

315316
private final UnaryOperator<Tuple> tupleWrapper;
316317

318+
private final boolean dtoProjection;
319+
320+
private final @Nullable PreferredConstructor<?, ?> preferredConstructor;
321+
317322
/**
318323
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
319324
*
@@ -336,6 +341,14 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) {
336341

337342
this.type = type;
338343
this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity();
344+
this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface()
345+
&& !type.getInputProperties().isEmpty();
346+
347+
if (this.dtoProjection) {
348+
this.preferredConstructor = PreferredConstructorDiscoverer.discover(String.class);
349+
} else {
350+
this.preferredConstructor = null;
351+
}
339352
}
340353

341354
@Override
@@ -356,23 +369,26 @@ public Object convert(Object source) {
356369
}
357370
}
358371

359-
if(type.isProjecting() && !type.getReturnedType().isInterface() && !type.getInputProperties().isEmpty()) {
360-
List<Object> ctorArgs = new ArrayList<>(type.getInputProperties().size());
361-
type.getInputProperties().forEach(it -> {
362-
ctorArgs.add(tuple.get(it));
363-
});
364-
try {
365-
return type.getReturnedType().getConstructor(ctorArgs.stream().map(Object::getClass).toArray(Class<?>[]::new)).newInstance(ctorArgs.toArray());
366-
} catch (InstantiationException e) {
367-
throw new RuntimeException(e);
368-
} catch (IllegalAccessException e) {
369-
throw new RuntimeException(e);
370-
} catch (InvocationTargetException e) {
371-
throw new RuntimeException(e);
372-
} catch (NoSuchMethodException e) {
373-
throw new RuntimeException(e);
374-
}
375-
}
372+
if (dtoProjection) {
373+
374+
Object[] ctorArgs = new Object[type.getInputProperties().size()];
375+
376+
for (int i = 0; i < type.getInputProperties().size(); i++) {
377+
ctorArgs[i] = tuple.get(i);
378+
}
379+
380+
try {
381+
382+
if (preferredConstructor.getParameterCount() == ctorArgs.length) {
383+
return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs);
384+
}
385+
386+
return BeanUtils.instantiateClass(type.getReturnedType()
387+
.getConstructor(Arrays.stream(ctorArgs).map(Object::getClass).toArray(Class<?>[]::new)), ctorArgs);
388+
} catch (ReflectiveOperationException e) {
389+
ReflectionUtils.handleReflectionException(e);
390+
}
391+
}
376392

377393
return new TupleBackedMap(tupleWrapper.apply(tuple));
378394
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

+16-29
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
6969
* @param valueExpressionDelegate must not be {@literal null}.
7070
*/
7171
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
72-
@Nullable String countQueryString, QueryRewriter queryRewriter,
73-
ValueExpressionDelegate valueExpressionDelegate) {
72+
@Nullable String countQueryString, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) {
7473

7574
super(method, em);
7675

@@ -99,15 +98,17 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
9998
});
10099

101100
this.queryRewriter = queryRewriter;
102-
ReturnedType returnedType = method.getResultProcessor().getReturnedType();
103101

104102
JpaParameters parameters = method.getParameters();
105-
if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) {
106-
this.querySortRewriter = new CachingQuerySortRewriter();
107-
} else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) {
108-
this.querySortRewriter = new ProjectingSortRewriter();
103+
104+
if (parameters.hasDynamicProjection()) {
105+
this.querySortRewriter = SimpleQuerySortRewriter.INSTANCE;
109106
} else {
110-
this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE;
107+
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
108+
this.querySortRewriter = new CachingQuerySortRewriter();
109+
} else {
110+
this.querySortRewriter = new UnsortedCachingQuerySortRewriter();
111+
}
111112
}
112113

113114
Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
@@ -119,19 +120,13 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
119120

120121
Sort sort = accessor.getSort();
121122
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
122-
123-
String sortedQueryString = null;
124-
if(querySortRewriter.equals(NoOpQuerySortRewriter.INSTANCE) && accessor.findDynamicProjection() != null && !accessor.findDynamicProjection().isInterface()) {
125-
sortedQueryString = getSortedQueryString(new ProjectingSortRewriter(), query, sort, processor.getReturnedType());
126-
} else {
127-
sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
128-
}
129-
130-
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
123+
ReturnedType returnedType = processor.getReturnedType();
124+
String sortedQueryString = getSortedQueryString(sort, returnedType);
125+
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType);
131126

132127
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
133128

134-
// it is ok to reuse the binding contained in the ParameterBinder although we create a new query String because the
129+
// it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the
135130
// parameters in the query do not change.
136131
return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
137132
}
@@ -140,10 +135,6 @@ String getSortedQueryString(Sort sort, ReturnedType returnedType) {
140135
return querySortRewriter.getSorted(query, sort, returnedType);
141136
}
142137

143-
private static String getSortedQueryString(QuerySortRewriter rewriter, DeclaredQuery query, Sort sort, ReturnedType returnedType) {
144-
return rewriter.getSorted(query, sort, returnedType);
145-
}
146-
147138
@Override
148139
protected ParameterBinder createBinder() {
149140
return createBinder(query);
@@ -237,21 +228,17 @@ interface QuerySortRewriter {
237228
/**
238229
* No-op query rewriter.
239230
*/
240-
enum NoOpQuerySortRewriter implements QuerySortRewriter {
231+
enum SimpleQuerySortRewriter implements QuerySortRewriter {
241232

242233
INSTANCE;
243234

244235
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
245236

246-
if (sort.isSorted()) {
247-
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
248-
}
249-
250-
return query.getQueryString();
237+
return QueryEnhancerFactory.forQuery(query).rewrite(sort, returnedType);
251238
}
252239
}
253240

254-
static class ProjectingSortRewriter implements QuerySortRewriter {
241+
static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter {
255242

256243
private volatile String cachedQueryString;
257244

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
4040

4141
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
4242

43-
if (!returnedType.isProjecting() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
43+
if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface()
44+
|| selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
4445
return selectionList;
4546
}
4647

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java

+5
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarExce
319319
}
320320
}
321321

322+
/**
323+
* Functional interface to rewrite a query considering {@link Sort} and {@link ReturnedType}. The function returns a
324+
* visitor object that can visit the parsed query tree.
325+
*/
326+
@FunctionalInterface
322327
interface SortedQueryRewriteFunction {
323328

324329
ParseTreeVisitor<? extends Object> apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType);

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java

+7
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ public interface QueryEnhancer {
8686
@Deprecated
8787
String applySorting(Sort sort, @Nullable String alias);
8888

89+
/**
90+
* Rewrite the query to include sorting and apply {@link ReturnedType} customizations.
91+
*
92+
* @param sort the sort specification to apply.
93+
* @param returnedType the type to be returned by the query.
94+
* @return the modified query string.
95+
*/
8996
String rewrite(Sort sort, ReturnedType returnedType);
9097

9198
/**

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java

+5-7
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,16 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
8383
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
8484
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
8585
this(entityPath, predicate, entityInformation, (Class<R>) entityInformation.getJavaType(), Sort.unsorted(), 0,
86-
Collections.emptySet(), finder, scrollQueryFactory,
87-
pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
86+
Collections.emptySet(), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
87+
projectionFactory);
8888
}
8989

9090
private FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
9191
JpaEntityInformation<S, ?> entityInformation, Class<R> resultType, Sort sort, int limit,
9292
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
9393
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
94-
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
95-
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
96-
EntityManager entityManager, ProjectionFactory projectionFactory) {
94+
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
95+
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
9796

9897
super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
9998
this.entityInformation = entityInformation;
@@ -142,8 +141,7 @@ public FetchableFluentQuery<R> project(Collection<String> properties) {
142141

143142
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
144143
mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation,
145-
entityManager,
146-
projectionFactory);
144+
entityManager, projectionFactory);
147145
}
148146

149147
@Override

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
import com.querydsl.core.types.Visitor;
3131
import com.querydsl.jpa.JPQLSerializer;
3232

33+
/**
34+
* Expression based on a {@link Tuple}. It's a simplified variant of {@link com.querydsl.core.types.QTuple} without
35+
* being a {@link com.querydsl.core.types.FactoryExpressionBase} as we do not want Querydsl to instantiate any tuples.
36+
* JPA is doing that for us.
37+
*/
3338
class JakartaTuple extends ExpressionBase<Tuple> {
3439

3540
private final List<Expression<?>> args;

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java

+18-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.data.jpa.provider.PersistenceProvider;
2626
import org.springframework.data.mapping.PropertyPath;
2727
import org.springframework.data.querydsl.QSort;
28+
import org.springframework.lang.Nullable;
2829
import org.springframework.util.Assert;
2930

3031
import com.querydsl.core.types.EntityPath;
@@ -37,6 +38,7 @@
3738
import com.querydsl.jpa.EclipseLinkTemplates;
3839
import com.querydsl.jpa.HQLTemplates;
3940
import com.querydsl.jpa.JPQLQuery;
41+
import com.querydsl.jpa.JPQLTemplates;
4042
import com.querydsl.jpa.impl.AbstractJPAQuery;
4143
import com.querydsl.jpa.impl.JPAQuery;
4244

@@ -77,10 +79,23 @@ public Querydsl(EntityManager em, PathBuilder<?> builder) {
7779
*/
7880
public <T> AbstractJPAQuery<T, JPAQuery<T>> createQuery() {
7981

82+
JPQLTemplates templates = getTemplates();
83+
return templates != null ? new SpringDataJpaQuery<>(em, templates) : new SpringDataJpaQuery<>(em);
84+
}
85+
86+
/**
87+
* Obtains the {@link JPQLTemplates} for the configured {@link EntityManager}. Can return {@literal null} to use the
88+
* default templates.
89+
*
90+
* @return the {@link JPQLTemplates} for the configured {@link EntityManager} or {@literal null} to use the default.
91+
*/
92+
@Nullable
93+
public JPQLTemplates getTemplates() {
94+
8095
return switch (provider) {
81-
case ECLIPSELINK -> new SpringDataJpaQuery<>(em, EclipseLinkTemplates.DEFAULT);
82-
case HIBERNATE -> new SpringDataJpaQuery<>(em, HQLTemplates.DEFAULT);
83-
default -> new SpringDataJpaQuery<>(em);
96+
case ECLIPSELINK -> EclipseLinkTemplates.DEFAULT;
97+
case HIBERNATE -> HQLTemplates.DEFAULT;
98+
default -> JPQLTemplates.DEFAULT;
8499
};
85100
}
86101

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
import com.querydsl.jpa.impl.JPAUtil;
3333

3434
/**
35+
* Customized String-Query implementation that specifically routes tuple query creation to
36+
* {@code EntityManager#createQuery(queryString, Tuple.class)}.
37+
*
3538
* @author Mark Paluch
3639
*/
3740
class SpringDataJpaQuery<T> extends JPAQuery<T> {

src/main/antora/modules/ROOT/pages/repositories/projections.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM U
4343
[WARNING]
4444
====
4545
JPQL constructor expressions must not contain aliases for selected columns.
46-
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 ...`. +
46+
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 `. +
4747
Some persistence providers may be lenient about this, others not.
4848
====
4949

0 commit comments

Comments
 (0)