Skip to content

Rewrite String queries for DTO Constructor Expressions #3654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data JPA Parent</name>
Expand Down
4 changes: 2 additions & 2 deletions spring-data-envers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>

<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-jpa-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jpa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>

<name>Spring Data JPA</name>
<description>Spring Data module for JPA repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.5.0-3076-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import jakarta.persistence.TupleElement;
import jakarta.persistence.TypedQuery;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
Expand All @@ -32,6 +34,8 @@
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
Expand All @@ -44,13 +48,16 @@
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;
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;

/**
* Abstract base class to implement {@link RepositoryQuery}s.
Expand Down Expand Up @@ -283,9 +290,10 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
return null;
}

return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
? Tuple.class //
: null;
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface()
&& !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
? Tuple.class //
: null;
}

/**
Expand All @@ -304,12 +312,16 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
*/
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);

static class TupleConverter implements Converter<Object, Object> {
public static class TupleConverter implements Converter<Object, Object> {

private final ReturnedType type;

private final UnaryOperator<Tuple> tupleWrapper;

private final boolean dtoProjection;

private final @Nullable PreferredConstructor<?, ?> preferredConstructor;

/**
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
*
Expand All @@ -332,6 +344,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(type.getReturnedType());
} else {
this.preferredConstructor = null;
}
}

@Override
Expand All @@ -352,9 +372,101 @@ public Object convert(Object source) {
}
}

if (dtoProjection) {

Object[] ctorArgs = new Object[elements.size()];
for (int i = 0; i < ctorArgs.length; i++) {
ctorArgs[i] = tuple.get(i);
}

List<Class<?>> argTypes = getArgumentTypes(ctorArgs);

if (preferredConstructor != null && isConstructorCompatible(preferredConstructor.getConstructor(), argTypes)) {
return BeanUtils.instantiateClass(preferredConstructor.getConstructor(), ctorArgs);
}

return BeanUtils.instantiateClass(getFirstMatchingConstructor(ctorArgs, argTypes), ctorArgs);
}

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

private Constructor<?> getFirstMatchingConstructor(Object[] ctorArgs, List<Class<?>> argTypes) {

for (Constructor<?> ctor : type.getReturnedType().getDeclaredConstructors()) {

if (ctor.getParameterCount() != ctorArgs.length) {
continue;
}

if (isConstructorCompatible(ctor, argTypes)) {
return ctor;
}
}

throw new IllegalStateException(String.format(
"Cannot find compatible constructor for DTO projection '%s' accepting '%s'", type.getReturnedType().getName(),
argTypes.stream().map(Class::getName).collect(Collectors.joining(", "))));
}

private static List<Class<?>> getArgumentTypes(Object[] ctorArgs) {
List<Class<?>> argTypes = new ArrayList<>(ctorArgs.length);

for (Object ctorArg : ctorArgs) {
argTypes.add(ctorArg == null ? Void.class : ctorArg.getClass());
}
return argTypes;
}

public static boolean isConstructorCompatible(Constructor<?> constructor, List<Class<?>> argumentTypes) {

if (constructor.getParameterCount() != argumentTypes.size()) {
return false;
}

for (int i = 0; i < argumentTypes.size(); i++) {

MethodParameter methodParameter = MethodParameter.forExecutable(constructor, i);
Class<?> argumentType = argumentTypes.get(i);

if (!areAssignmentCompatible(methodParameter.getParameterType(), argumentType)) {
return false;
}
}
return true;
}

private static boolean areAssignmentCompatible(Class<?> to, Class<?> from) {

if (from == Void.class && !to.isPrimitive()) {
// treat Void as the bottom type, the class of null
return true;
}

if (to.isPrimitive()) {

if (to == Short.TYPE) {
return from == Character.class || from == Byte.class;
}

if (to == Integer.TYPE) {
return from == Short.class || from == Character.class || from == Byte.class;
}

if (to == Long.TYPE) {
return from == Integer.class || from == Short.class || from == Character.class || from == Byte.class;
}

if (to == Double.TYPE) {
return from == Float.class;
}

return ClassUtils.isAssignable(to, from);
}

return ClassUtils.isAssignable(to, from);
}

/**
* A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
* {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -101,10 +100,15 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
this.queryRewriter = queryRewriter;

JpaParameters parameters = method.getParameters();
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
this.querySortRewriter = new CachingQuerySortRewriter();

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(),
Expand All @@ -115,21 +119,20 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {

Sort sort = accessor.getSort();
String sortedQueryString = getSortedQueryString(sort);

ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);

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

String getSortedQueryString(Sort sort) {
return querySortRewriter.getSorted(query, sort);
String getSortedQueryString(Sort sort, ReturnedType returnedType) {
return querySortRewriter.getSorted(query, sort, returnedType);
}

@Override
Expand Down Expand Up @@ -211,30 +214,47 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla

String applySorting(CachableQuery cachableQuery) {

return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(),
cachableQuery.getAlias());
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery())
.rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType()));
}

/**
* Query Sort Rewriter interface.
*/
interface QuerySortRewriter {
String getSorted(DeclaredQuery query, Sort sort);
String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType);
}

/**
* No-op query rewriter.
*/
enum NoOpQuerySortRewriter implements QuerySortRewriter {
enum SimpleQuerySortRewriter implements QuerySortRewriter {

INSTANCE;

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

return QueryEnhancerFactory.forQuery(query).rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
}

static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter {

private volatile String cachedQueryString;

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

if (sort.isSorted()) {
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
}

return query.getQueryString();
String cachedQueryString = this.cachedQueryString;
if (cachedQueryString == null) {
this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query)
.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}

return cachedQueryString;
}
}

Expand All @@ -246,14 +266,22 @@ class CachingQuerySortRewriter implements QuerySortRewriter {
private final ConcurrentLruCache<CachableQuery, String> queryCache = new ConcurrentLruCache<>(16,
AbstractStringBasedJpaQuery.this::applySorting);

private volatile String cachedQueryString;

@Override
public String getSorted(DeclaredQuery query, Sort sort) {
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {

if (sort.isUnsorted()) {
return query.getQueryString();

String cachedQueryString = this.cachedQueryString;
if (cachedQueryString == null) {
this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType));
}

return cachedQueryString;
}

return queryCache.get(new CachableQuery(query, sort));
return queryCache.get(new CachableQuery(query, sort, returnedType));
}
}

Expand All @@ -269,12 +297,14 @@ static class CachableQuery {
private final DeclaredQuery declaredQuery;
private final String queryString;
private final Sort sort;
private final ReturnedType returnedType;

CachableQuery(DeclaredQuery query, Sort sort) {
CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) {

this.declaredQuery = query;
this.queryString = query.getQueryString();
this.sort = sort;
this.returnedType = returnedType;
}

DeclaredQuery getDeclaredQuery() {
Expand All @@ -285,9 +315,8 @@ Sort getSort() {
return sort;
}

@Nullable
String getAlias() {
return declaredQuery.getAlias();
public ReturnedType getReturnedType() {
return returnedType;
}

@Override
Expand Down
Loading