Polishing.

Simplify Querydsl templates retrieve and String query caching. Update documentation. Skip selection list rewriting if the returned type is an interface.

Encapsulate rewrite information for Query.

Reformat code.

See: #2327
Original Pull Request: #3654
This commit is contained in:
Mark Paluch
2024-12-03 09:10:55 +01:00
committed by Christoph Strobl
parent 755fe2623f
commit 5d41ada346
13 changed files with 197 additions and 87 deletions

View File

@@ -23,8 +23,6 @@ import jakarta.persistence.Tuple;
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.Set;
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.SlicedExe
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.data.util.Lazy;
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 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface()
&& !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
? Tuple.class //
: null;
? Tuple.class //
: null;
}
/**
@@ -314,6 +315,10 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
private final UnaryOperator<Tuple> 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 abstract class AbstractJpaQuery implements RepositoryQuery {
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 abstract class AbstractJpaQuery implements RepositoryQuery {
}
}
if(type.isProjecting() && !type.getReturnedType().isInterface() && !type.getInputProperties().isEmpty()) {
List<Object> 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));
}

View File

@@ -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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
});
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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
/**
* 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 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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;

View File

@@ -18,7 +18,6 @@ package org.springframework.data.jpa.repository.query;
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 class DefaultQueryEnhancer implements QueryEnhancer {
}
@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

View File

@@ -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();
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.data.repository.query.ReturnedType;
* </ul>
*
* @author Mark Paluch
* @since 3.5
*/
class DtoProjectionTransformerDelegate {
@@ -40,7 +41,8 @@ class DtoProjectionTransformerDelegate {
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;
}

View File

@@ -50,7 +50,6 @@ import java.util.Set;
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 class JSqlParserQueryEnhancer implements QueryEnhancer {
@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");

View File

@@ -200,8 +200,9 @@ class JpaQueryEnhancer implements QueryEnhancer {
}
@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 @@ class JpaQueryEnhancer implements QueryEnhancer {
}
}
/**
* 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<? extends Object> apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType);

View File

@@ -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 @@ public interface QueryEnhancer {
*/
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();
}
}

View File

@@ -83,17 +83,16 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
this(entityPath, predicate, entityInformation, (Class<R>) 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<S, ?> entityInformation, Class<R> resultType, Sort sort, int limit,
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
EntityManager entityManager, ProjectionFactory projectionFactory) {
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
this.entityInformation = entityInformation;
@@ -142,8 +141,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
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 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
}
}
/**
* @since 3.5
*/
private static class DtoProjection extends ExpressionBase<Object> {
private final Expression<?>[] projection;

View File

@@ -20,6 +20,8 @@ import jakarta.persistence.Tuple;
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.Projections;
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<Tuple> {
private final List<Expression<?>> args;
@@ -61,7 +72,8 @@ class JakartaTuple extends ExpressionBase<Tuple> {
}
@Override
public <R, C> R accept(Visitor<R, C> v, C context) {
@Nullable
public <R, C> R accept(Visitor<R, C> v, @Nullable C context) {
if (v instanceof JPQLSerializer) {
return Projections.tuple(args).accept(v, context);
@@ -74,8 +86,7 @@ class JakartaTuple extends ExpressionBase<Tuple> {
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;

View File

@@ -25,6 +25,7 @@ import org.springframework.data.domain.Sort.Order;
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.core.types.dsl.PathBuilder;
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 class Querydsl {
*/
public <T> AbstractJPAQuery<T, JPAQuery<T>> 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 @@ public class Querydsl {
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;
};
}
/**

View File

@@ -32,7 +32,11 @@ import com.querydsl.jpa.impl.JPAQuery;
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<T> extends JPAQuery<T> {

View File

@@ -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.
====