Refrain from rewriting queries without input properties.

We now no longer attempt to rewrite the query if the target type doesn't define input properties (no-args constructor or multiple constructors).

Closes #3895
This commit is contained in:
Mark Paluch
2025-06-02 10:54:06 +02:00
parent fa9b6815ce
commit c538a4fcb4
5 changed files with 158 additions and 249 deletions
@@ -42,7 +42,7 @@ class DtoProjectionTransformerDelegate {
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface()
|| selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
|| !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
return selectionList;
}
@@ -0,0 +1,133 @@
/*
* Copyright 2024-2025 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.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.junit.jupiter.api.Test;
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;
import org.springframework.data.repository.query.QueryMethod;
/**
* Support class for unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<? extends QueryInformation>> {
JpaQueryMethod method = getMethod("dtoProjection");
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
P parser = parse("SELECT p from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
}
@Test // GH-3076
void shouldRewriteQueriesWithSubselect() {
P parser = parse("select u from User u left outer join u.roles r where r in (select r from Role r)");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"select new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$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 shouldNotRewriteQueriesWithoutProperties() {
JpaQueryMethod method = getMethod("noProjection");
P parser = parse("select u from User u");
QueryTokenStream visit = getTransformer(parser, method).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("select u from User u");
}
@Test // GH-3076
void shouldNotTranslateConstructorExpressionQuery() {
P parser = parse("SELECT NEW com.foo(p) from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW com.foo(p) from Person p");
}
@Test
void shouldTranslatePropertySelectionToDto() {
P parser = parse("SELECT p.foo, p.bar, sum(p.age) from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$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);
}
}
abstract P parse(String query);
private ParseTreeVisitor<QueryTokenStream> getTransformer(P parser) {
return getTransformer(parser, method);
}
abstract ParseTreeVisitor<QueryTokenStream> getTransformer(P parser, QueryMethod method);
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
EmptyClass noProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
static class EmptyClass {
}
}
@@ -15,102 +15,26 @@
*/
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.antlr.v4.runtime.tree.ParseTreeVisitor;
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;
import org.springframework.data.repository.query.QueryMethod;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class EqlDtoQueryTransformerUnitTests {
class EqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests<JpaQueryEnhancer.EqlQueryParser> {
JpaQueryMethod method = getMethod("dtoProjection");
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = getTransformer(parser).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");
@Override
JpaQueryEnhancer.EqlQueryParser parse(String query) {
return JpaQueryEnhancer.EqlQueryParser.parseQuery(query);
}
@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 = getTransformer(parser).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 Foo(p) from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p");
}
@Test
void shouldTranslatePropertySelectionToDto() {
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
EqlSortedQueryTransformer transformer = getTransformer(parser);
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);
}
}
private EqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.EqlQueryParser parser) {
@Override
ParseTreeVisitor<QueryTokenStream> getTransformer(JpaQueryEnhancer.EqlQueryParser parser, QueryMethod method) {
return new EqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(),
method.getResultProcessor().getReturnedType());
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}
@@ -15,101 +15,27 @@
*/
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.antlr.v4.runtime.tree.ParseTreeVisitor;
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;
import org.springframework.data.repository.query.QueryMethod;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class HqlDtoQueryTransformerUnitTests {
class HqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests<JpaQueryEnhancer.HqlQueryParser> {
JpaQueryMethod method = getMethod("dtoProjection");
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = getTransformer(parser).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");
@Override
JpaQueryEnhancer.HqlQueryParser parse(String query) {
return JpaQueryEnhancer.HqlQueryParser.parseQuery(query);
}
@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 = getTransformer(parser).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 = getTransformer(parser).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 = getTransformer(parser).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);
}
}
private HqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.HqlQueryParser parser) {
@Override
ParseTreeVisitor<QueryTokenStream> getTransformer(JpaQueryEnhancer.HqlQueryParser parser, QueryMethod method) {
return new HqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(),
method.getResultProcessor().getReturnedType());
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}
@@ -15,101 +15,27 @@
*/
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.antlr.v4.runtime.tree.ParseTreeVisitor;
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;
import org.springframework.data.repository.query.QueryMethod;
/**
* Unit tests for {@link DtoProjectionTransformerDelegate}.
*
* @author Mark Paluch
*/
class JpqlDtoQueryTransformerUnitTests {
class JpqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests<JpaQueryEnhancer.JpqlQueryParser> {
JpaQueryMethod method = getMethod("dtoProjection");
@Test // GH-3076
void shouldTranslateSingleProjectionToDto() {
JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p");
QueryTokenStream visit = getTransformer(parser).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");
@Override
JpaQueryEnhancer.JpqlQueryParser parse(String query) {
return JpaQueryEnhancer.JpqlQueryParser.parseQuery(query);
}
@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 = getTransformer(parser).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 Foo(p) from Person p");
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(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 = getTransformer(parser).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);
}
}
private JpqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.JpqlQueryParser parser) {
@Override
ParseTreeVisitor<QueryTokenStream> getTransformer(JpaQueryEnhancer.JpqlQueryParser parser, QueryMethod method) {
return new JpqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(),
method.getResultProcessor().getReturnedType());
}
interface MyRepo extends Repository<Person, String> {
MyRecord dtoProjection();
}
record Person(String id) {
}
record MyRecord(String foo, String bar) {
}
}