Rewrite string-queries to use constructor expressions when return type is DTO.

We now rewrite String-based JPA queries to use constructor expressions when either selecting the entity or selecting individual properties.

We do not rewrite queries that already use constructor expressions.

Closes: #3076
Original Pull Request: #3654
This commit is contained in:
Mark Paluch
2024-08-13 14:25:08 +02:00
committed by Christoph Strobl
parent 12b5e8205d
commit 1341c3f14e
20 changed files with 650 additions and 37 deletions

View File

@@ -283,7 +283,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return null;
}
return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface()
&& !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
? Tuple.class //
: null;
}

View File

@@ -99,10 +99,13 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
});
this.queryRewriter = queryRewriter;
ReturnedType returnedType = method.getResultProcessor().getReturnedType();
JpaParameters parameters = method.getParameters();
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
if ((parameters.hasPageableParameter() || parameters.hasSortParameter()) && !parameters.hasDynamicProjection()) {
this.querySortRewriter = new CachingQuerySortRewriter();
} else if (returnedType.isProjecting() && !returnedType.getReturnedType().isInterface()) {
this.querySortRewriter = new ProjectingSortRewriter();
} else {
this.querySortRewriter = NoOpQuerySortRewriter.INSTANCE;
}
@@ -115,9 +118,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
Sort sort = accessor.getSort();
String sortedQueryString = getSortedQueryString(sort);
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
String sortedQueryString = getSortedQueryString(sort, processor.getReturnedType());
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
@@ -128,8 +130,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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
@@ -211,24 +213,25 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
String applySorting(CachableQuery cachableQuery) {
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(),
cachableQuery.getAlias());
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).rewrite(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 {
INSTANCE;
public String getSorted(DeclaredQuery query, Sort sort) {
public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isSorted()) {
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
@@ -238,6 +241,25 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
}
}
static class ProjectingSortRewriter 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");
}
String cachedQueryString = this.cachedQueryString;
if (cachedQueryString == null) {
this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query).rewrite(sort, returnedType);
}
return cachedQueryString;
}
}
/**
* Caching variant of {@link QuerySortRewriter}.
*/
@@ -246,14 +268,22 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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));
}
}
@@ -269,12 +299,14 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
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() {
@@ -285,9 +317,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return sort;
}
@Nullable
String getAlias() {
return declaredQuery.getAlias();
public ReturnedType getReturnedType() {
return returnedType;
}
@Override

View File

@@ -18,6 +18,7 @@ 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;
/**
@@ -52,6 +53,11 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
@Override
public String rewrite(Sort sort, ReturnedType returnedType) {
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
@Override
public String createCountQueryFor(@Nullable String countProjection) {
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery());

View File

@@ -0,0 +1,73 @@
/*
* 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 static org.springframework.data.jpa.repository.query.QueryTokens.*;
import org.springframework.data.repository.query.ReturnedType;
/**
* HQL Query Transformer that rewrites the query using constructor expressions.
* <p>
* Query rewriting from a plain property/object selection towards constructor expression only works if either:
* <ul>
* <li>The query selects its primary alias ({@code SELECT p FROM Person p})</li>
* <li>The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})</li>
* </ul>
*
* @author Mark Paluch
*/
class DtoProjectionTransformerDelegate {
private final ReturnedType returnedType;
public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
this.returnedType = returnedType;
}
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
if (!returnedType.isProjecting() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
return selectionList;
}
QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(QueryTokens.TOKEN_NEW);
builder.append(QueryTokens.token(returnedType.getReturnedType().getName()));
builder.append(QueryTokens.TOKEN_OPEN_PAREN);
// assume the selection points to the document
if (selectionList.size() == 1) {
builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> {
QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder();
prop.append(QueryTokens.token(selectionList.getFirst().value()));
prop.append(QueryTokens.TOKEN_DOT);
prop.append(QueryTokens.token(property));
return prop.build();
}, QueryTokens.TOKEN_COMMA));
} else {
builder.appendInline(selectionList);
}
builder.append(QueryTokens.TOKEN_CLOSE_PAREN);
return builder.build();
}
}

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -40,13 +41,15 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -80,6 +83,26 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
return builder;
}
@Override
public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) {
if (dtoDelegate == null) {
return super.visitSelect_clause(ctx);
}
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(QueryTokens.expression(ctx.SELECT()));
if (ctx.DISTINCT() != null) {
builder.append(QueryTokens.expression(ctx.DISTINCT()));
}
QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
return builder.append(dtoDelegate.transformSelectionList(tokenStream));
}
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) {
if (ctx.orderby_clause() != null) {

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -38,6 +39,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
@@ -45,6 +47,16 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
this.dtoDelegate = null;
}
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -81,6 +93,18 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
return visitOrderedQuery(ctx, this.sort);
}
@Override
public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) {
QueryTokenStream tokenStream = super.visitSelectionList(ctx);
if (dtoDelegate != null && !isSubquery(ctx)) {
return dtoDelegate.transformSelectionList(tokenStream);
}
return tokenStream;
}
@Override
public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) {

View File

@@ -50,6 +50,7 @@ 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;
@@ -300,6 +301,11 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
return applySorting(sort, detectAlias());
}
@Override
public String rewrite(Sort sort, ReturnedType returnedType) {
return applySorting(sort, primaryAlias);
}
@Override
public String applySorting(Sort sort, @Nullable String alias) {

View File

@@ -30,6 +30,7 @@ import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -48,12 +49,12 @@ class JpaQueryEnhancer implements QueryEnhancer {
private final ParserRuleContext context;
private final ParsedQueryIntrospector introspector;
private final String projection;
private final BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction;
private final SortedQueryRewriteFunction sortFunction;
private final BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction;
JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector,
@Nullable BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction,
@Nullable BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction) {
SortedQueryRewriteFunction sortFunction,
BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction) {
this.context = context;
this.introspector = introspector;
@@ -136,6 +137,10 @@ class JpaQueryEnhancer implements QueryEnhancer {
return EqlQueryParser.parseQuery(query.getQueryString());
}
ParserRuleContext getContext() {
return context;
}
/**
* Checks if the select clause has a new constructor instantiation in the JPA query.
*
@@ -191,7 +196,12 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String applySorting(Sort sort) {
return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias()).visit(context));
return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), null).visit(context));
}
@Override
public String rewrite(Sort sort, ReturnedType returnedType) {
return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), returnedType).visit(context));
}
/**
@@ -308,4 +318,10 @@ class JpaQueryEnhancer implements QueryEnhancer {
return new JpqlQueryParser(query);
}
}
interface SortedQueryRewriteFunction {
ParseTreeVisitor<? extends Object> apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType);
}
}

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -38,6 +39,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
private final @Nullable DtoProjectionTransformerDelegate dtoDelegate;
JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
@@ -45,6 +47,16 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
this.dtoDelegate = null;
}
JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType);
}
@Override
@@ -72,6 +84,26 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
return builder;
}
@Override
public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
if (dtoDelegate == null) {
return super.visitSelect_clause(ctx);
}
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(QueryTokens.expression(ctx.SELECT()));
if (ctx.DISTINCT() != null) {
builder.append(QueryTokens.expression(ctx.DISTINCT()));
}
QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA);
return builder.append(dtoDelegate.transformSelectionList(tokenStream));
}
private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) {
if (ctx.orderby_clause() != null) {

View File

@@ -18,6 +18,7 @@ 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;
/**
@@ -85,6 +86,8 @@ public interface QueryEnhancer {
@Deprecated
String applySorting(Sort sort, @Nullable String alias);
String rewrite(Sort sort, ReturnedType returnedType);
/**
* Creates a count projected query from the given original query.
*
@@ -101,4 +104,5 @@ public interface QueryEnhancer {
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
*/
String createCountQueryFor(@Nullable String countProjection);
}

View File

@@ -38,6 +38,7 @@ class QueryTokens {
static final QueryToken TOKEN_EQUALS = token(" = ");
static final QueryToken TOKEN_OPEN_PAREN = token("(");
static final QueryToken TOKEN_CLOSE_PAREN = token(")");
static final QueryToken TOKEN_NEW = expression("new");
static final QueryToken TOKEN_ORDER_BY = expression("order by");
static final QueryToken TOKEN_LOWER_FUNC = token("lower(");
static final QueryToken TOKEN_SELECT_COUNT = token("select count(");

View File

@@ -61,7 +61,7 @@ class QueryTransformers {
token = QueryTokens.token(token.value());
}
if (!containsNew && token.value().contains("new")) {
if (!containsNew && token.equals(TOKEN_NEW)) {
containsNew = true;
}

View File

@@ -378,7 +378,7 @@ class UserRepositoryFinderTests {
}
@Test // DATAJPA-1713, GH-2008
public void selectProjectionWithSubselect() {
void selectProjectionWithSubselect() {
List<UserRepository.NameOnly> dtos = userRepository.findProjectionBySubselect();
@@ -405,4 +405,22 @@ class UserRepositoryFinderTests {
result = userRepository.findUserByLastname(carter.getLastname());
assertThat(result).containsExactly(carter);
}
@Test // GH-3076
void dtoProjectionShouldApplyConstructorExpressionRewriting() {
List<UserRepository.UserExcerpt> dtos = userRepository.findRecordProjection();
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
.contains("Dave", "Carter", "Oliver August");
}
@Test // GH-3076
void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() {
List<UserRepository.UserExcerpt> dtos = userRepository.findMultiselectRecordProjection();
assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) //
.contains("Dave", "Carter", "Oliver August");
}
}

View File

@@ -52,6 +52,7 @@ import org.springframework.util.ReflectionUtils;
* Unit tests for {@link AbstractStringBasedJpaQuery}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class AbstractStringBasedJpaQueryUnitTests {
@@ -64,13 +65,14 @@ class AbstractStringBasedJpaQueryUnitTests {
stringQuery.neverCalled("applySorting");
}
@Test // GH-3310
void shouldNotAttemptToAppendSortIfSortIndicatesUnsorted() {
@Test // GH-3310, GH-3076
void shouldRunQueryRewriterOnce() {
InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find", Sort.class);
stringQuery.createQueryWithArguments(Sort.unsorted());
stringQuery.createQueryWithArguments(Sort.unsorted());
stringQuery.neverCalled("applySorting");
stringQuery.called("applySorting").times(1);
}
@Test // GH-3310

View File

@@ -0,0 +1,112 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class EqlDtoQueryTransformerUnitTests {
JpaQueryMethod method = getMethod("dtoProjection");
EqlSortedQueryTransformer transformer = new EqlSortedQueryTransformer(Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}
@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
.parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"select new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)");
}
@Test // GH-3076
void shouldNotTranslateConstructorExpressionQuery() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
.parseQuery("SELECT NEW String(p) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p");
}
@Test
void shouldTranslatePropertySelectionToDto() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
}
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
try {
Method method = MyRepo.class.getMethod(name, parameterTypes);
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
new SpelAwareProxyProjectionFactory(), persistenceProvider);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class HqlDtoQueryTransformerUnitTests {
JpaQueryMethod method = getMethod("dtoProjection");
HqlSortedQueryTransformer transformer = new HqlSortedQueryTransformer(Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}
@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
.parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"select new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)");
}
@Test // GH-3076
void shouldNotTranslateConstructorExpressionQuery() {
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
.parseQuery("SELECT NEW String(p) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p");
}
@Test
void shouldTranslatePropertySelectionToDto() {
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
}
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
try {
Method method = MyRepo.class.getMethod(name, parameterTypes);
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
new SpelAwareProxyProjectionFactory(), persistenceProvider);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class JpqlDtoQueryTransformerUnitTests {
JpaQueryMethod method = getMethod("dtoProjection");
JpqlSortedQueryTransformer transformer = new JpqlSortedQueryTransformer(Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}
@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {
JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser
.parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)");
}
@Test // GH-3076
void shouldNotTranslateConstructorExpressionQuery() {
JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser
.parseQuery("SELECT NEW String(p) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p");
}
@Test
void shouldTranslatePropertySelectionToDto() {
JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
QueryTokenStream visit = transformer.visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
}
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
try {
Method method = MyRepo.class.getMethod(name, parameterTypes);
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
new SpelAwareProxyProjectionFactory(), persistenceProvider);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}

View File

@@ -65,15 +65,8 @@ class NativeJpaQueryUnitTests {
@Test // GH-3546
void shouldApplySorting() {
NativeJpaQuery query = getQuery(TestRepo.class, "find", Sort.class);
String sql = query.getSortedQueryString(Sort.by("foo", "bar"));
assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc");
}
private NativeJpaQuery getQuery(Class<?> repository, String method, Class<?>... args) {
Method respositoryMethod = ReflectionUtils.findMethod(repository, method, args);
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(repository);
Method respositoryMethod = ReflectionUtils.findMethod(TestRepo.class, "find", Sort.class);
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(TestRepo.class);
SpelAwareProxyProjectionFactory projectionFactory = mock(SpelAwareProxyProjectionFactory.class);
QueryExtractor queryExtractor = mock(QueryExtractor.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory,
@@ -83,7 +76,9 @@ class NativeJpaQueryUnitTests {
NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(),
QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
return query;
String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType());
assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc");
}
interface TestRepo extends Repository<Object, Object> {

View File

@@ -721,6 +721,12 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
@Query("select u from User u where u.firstname >= (select Min(u0.firstname) from User u0)")
List<NameOnly> findProjectionBySubselect();
@Query("select u from User u")
List<UserExcerpt> findRecordProjection();
@Query("select u.firstname, u.lastname from User u")
List<UserExcerpt> findMultiselectRecordProjection();
Window<User> findBy(OffsetScrollPosition position);
interface RolesAndFirstname {
@@ -747,4 +753,8 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
int getId();
}
record UserExcerpt(String firstname, String lastname) {
}
}

View File

@@ -36,6 +36,41 @@ When using <<projections.dtos,Class-based projections>> with JPQL, you must use
(Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in `@Query` annotations as well where you define any named queries.
As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[]
===== DTO Projection JPQL Query Rewriting
JPQL queries allow selection of the root object, individual properties, and DTO objects through constructor expressions.
Using a constructor expression can quickly add a lot of text to a query and make it difficult to read the actual query.
Spring Data JPA can support you with your JPQL queries by introducing constructor expressions for your convenience.
Consider the following queries:
.Projection Queries
====
[source,java]
----
interface UserRepository extends Repository<User, Long> {
@Query("SELECT u FROM USER u") <1>
List<UserDto> findByLastname(String lastname);
@Query("SELECT u.firstname, u.lastname FROM USER u") <2>
List<UserDto> findMultipleColumnsByLastname(String lastname);
}
record UserDto(String firstname, String lastname){}
----
<1> Selection of the top-level entity.
This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`.
<2> Multi-select of `firstname` and `lastname` properties.
This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`.
====
Repository query methods that return a DTO projection type (a Java type outside the domain type hierarchy) are subject for query rewriting.
If an `@Query`-annotated query already uses constructor expressions, then Spring Data backs off and doesn't apply DTO constructor expression rewriting.
Make sure that your DTO types provide an all-args constructor for the projection, otherwise the query will fail.
==== Native Queries
When using <<projections.dtos,Class-based projections>>, their usage requires slightly more consideration depending on your :