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:
committed by
Christoph Strobl
parent
12b5e8205d
commit
1341c3f14e
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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(");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 :
|
||||
|
||||
Reference in New Issue
Block a user