Skip to content

Commit c538a4f

Browse files
committed
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
1 parent fa9b681 commit c538a4f

File tree

5 files changed

+158
-249
lines changed

5 files changed

+158
-249
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public DtoProjectionTransformerDelegate(ReturnedType returnedType) {
4242
public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) {
4343

4444
if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface()
45-
|| selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
45+
|| !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) {
4646
return selectionList;
4747
}
4848

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import java.lang.reflect.Method;
21+
22+
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.data.jpa.provider.PersistenceProvider;
26+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
27+
import org.springframework.data.repository.Repository;
28+
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
29+
import org.springframework.data.repository.query.QueryMethod;
30+
31+
/**
32+
* Support class for unit tests for {@link DtoProjectionTransformerDelegate}.
33+
*
34+
* @author Mark Paluch
35+
*/
36+
abstract class AbstractDtoQueryTransformerUnitTests<P extends JpaQueryEnhancer<? extends QueryInformation>> {
37+
38+
JpaQueryMethod method = getMethod("dtoProjection");
39+
40+
@Test // GH-3076
41+
void shouldTranslateSingleProjectionToDto() {
42+
43+
P parser = parse("SELECT p from Person p");
44+
45+
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
46+
47+
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
48+
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
49+
}
50+
51+
@Test // GH-3076
52+
void shouldRewriteQueriesWithSubselect() {
53+
54+
P parser = parse("select u from User u left outer join u.roles r where r in (select r from Role r)");
55+
56+
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
57+
58+
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
59+
"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)");
60+
}
61+
62+
@Test // GH-3076
63+
void shouldNotRewriteQueriesWithoutProperties() {
64+
65+
JpaQueryMethod method = getMethod("noProjection");
66+
P parser = parse("select u from User u");
67+
68+
QueryTokenStream visit = getTransformer(parser, method).visit(parser.getContext());
69+
70+
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("select u from User u");
71+
}
72+
73+
@Test // GH-3076
74+
void shouldNotTranslateConstructorExpressionQuery() {
75+
76+
P parser = parse("SELECT NEW com.foo(p) from Person p");
77+
78+
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
79+
80+
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW com.foo(p) from Person p");
81+
}
82+
83+
@Test
84+
void shouldTranslatePropertySelectionToDto() {
85+
86+
P parser = parse("SELECT p.foo, p.bar, sum(p.age) from Person p");
87+
88+
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
89+
90+
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
91+
"SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
92+
}
93+
94+
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
95+
96+
try {
97+
Method method = MyRepo.class.getMethod(name, parameterTypes);
98+
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
99+
100+
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
101+
new SpelAwareProxyProjectionFactory(), persistenceProvider);
102+
} catch (NoSuchMethodException e) {
103+
throw new RuntimeException(e);
104+
}
105+
}
106+
107+
abstract P parse(String query);
108+
109+
private ParseTreeVisitor<QueryTokenStream> getTransformer(P parser) {
110+
return getTransformer(parser, method);
111+
}
112+
113+
abstract ParseTreeVisitor<QueryTokenStream> getTransformer(P parser, QueryMethod method);
114+
115+
interface MyRepo extends Repository<Person, String> {
116+
117+
MyRecord dtoProjection();
118+
119+
EmptyClass noProjection();
120+
}
121+
122+
record Person(String id) {
123+
124+
}
125+
126+
record MyRecord(String foo, String bar) {
127+
128+
}
129+
130+
static class EmptyClass {
131+
132+
}
133+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java

Lines changed: 8 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -15,102 +15,26 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.assertj.core.api.Assertions.*;
19-
20-
import java.lang.reflect.Method;
21-
22-
import org.junit.jupiter.api.Test;
18+
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
2319

2420
import org.springframework.data.domain.Sort;
25-
import org.springframework.data.jpa.provider.PersistenceProvider;
26-
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
27-
import org.springframework.data.repository.Repository;
28-
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
21+
import org.springframework.data.repository.query.QueryMethod;
2922

3023
/**
3124
* Unit tests for {@link DtoProjectionTransformerDelegate}.
3225
*
3326
* @author Mark Paluch
3427
*/
35-
class EqlDtoQueryTransformerUnitTests {
36-
37-
JpaQueryMethod method = getMethod("dtoProjection");
38-
39-
@Test // GH-3076
40-
void shouldTranslateSingleProjectionToDto() {
41-
42-
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p");
43-
44-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
45-
46-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
47-
"SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
48-
}
49-
50-
@Test // GH-3076
51-
void shouldRewriteQueriesWithSubselect() {
52-
53-
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
54-
.parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)");
55-
56-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
57-
58-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
59-
"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)");
60-
}
61-
62-
@Test // GH-3076
63-
void shouldNotTranslateConstructorExpressionQuery() {
64-
65-
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
66-
.parseQuery("SELECT NEW Foo(p) from Person p");
67-
68-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
69-
70-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p");
71-
}
72-
73-
@Test
74-
void shouldTranslatePropertySelectionToDto() {
75-
76-
JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser
77-
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
28+
class EqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests<JpaQueryEnhancer.EqlQueryParser> {
7829

79-
EqlSortedQueryTransformer transformer = getTransformer(parser);
80-
QueryTokenStream visit = transformer.visit(parser.getContext());
81-
82-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
83-
"SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
30+
@Override
31+
JpaQueryEnhancer.EqlQueryParser parse(String query) {
32+
return JpaQueryEnhancer.EqlQueryParser.parseQuery(query);
8433
}
8534

86-
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
87-
88-
try {
89-
Method method = MyRepo.class.getMethod(name, parameterTypes);
90-
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
91-
92-
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
93-
new SpelAwareProxyProjectionFactory(), persistenceProvider);
94-
} catch (NoSuchMethodException e) {
95-
throw new RuntimeException(e);
96-
}
97-
}
98-
99-
private EqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.EqlQueryParser parser) {
35+
@Override
36+
ParseTreeVisitor<QueryTokenStream> getTransformer(JpaQueryEnhancer.EqlQueryParser parser, QueryMethod method) {
10037
return new EqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(),
10138
method.getResultProcessor().getReturnedType());
10239
}
103-
104-
interface MyRepo extends Repository<Person, String> {
105-
106-
MyRecord dtoProjection();
107-
}
108-
109-
record Person(String id) {
110-
111-
}
112-
113-
record MyRecord(String foo, String bar) {
114-
115-
}
11640
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java

Lines changed: 8 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -15,101 +15,27 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.assertj.core.api.Assertions.*;
19-
20-
import java.lang.reflect.Method;
21-
22-
import org.junit.jupiter.api.Test;
18+
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
2319

2420
import org.springframework.data.domain.Sort;
25-
import org.springframework.data.jpa.provider.PersistenceProvider;
26-
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
27-
import org.springframework.data.repository.Repository;
28-
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
21+
import org.springframework.data.repository.query.QueryMethod;
2922

3023
/**
3124
* Unit tests for {@link DtoProjectionTransformerDelegate}.
3225
*
3326
* @author Mark Paluch
3427
*/
35-
class HqlDtoQueryTransformerUnitTests {
36-
37-
JpaQueryMethod method = getMethod("dtoProjection");
38-
39-
@Test // GH-3076
40-
void shouldTranslateSingleProjectionToDto() {
41-
42-
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p");
28+
class HqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests<JpaQueryEnhancer.HqlQueryParser> {
4329

44-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
45-
46-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
47-
"SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p");
30+
@Override
31+
JpaQueryEnhancer.HqlQueryParser parse(String query) {
32+
return JpaQueryEnhancer.HqlQueryParser.parseQuery(query);
4833
}
4934

50-
@Test // GH-3076
51-
void shouldRewriteQueriesWithSubselect() {
52-
53-
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
54-
.parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)");
55-
56-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
57-
58-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
59-
"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)");
60-
}
61-
62-
@Test // GH-3076
63-
void shouldNotTranslateConstructorExpressionQuery() {
64-
65-
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
66-
.parseQuery("SELECT NEW String(p) from Person p");
67-
68-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
69-
70-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p");
71-
}
72-
73-
@Test
74-
void shouldTranslatePropertySelectionToDto() {
75-
76-
JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser
77-
.parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p");
78-
79-
QueryTokenStream visit = getTransformer(parser).visit(parser.getContext());
80-
81-
assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo(
82-
"SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p");
83-
}
84-
85-
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) {
86-
87-
try {
88-
Method method = MyRepo.class.getMethod(name, parameterTypes);
89-
PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE;
90-
91-
return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class),
92-
new SpelAwareProxyProjectionFactory(), persistenceProvider);
93-
} catch (NoSuchMethodException e) {
94-
throw new RuntimeException(e);
95-
}
96-
}
97-
98-
private HqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.HqlQueryParser parser) {
35+
@Override
36+
ParseTreeVisitor<QueryTokenStream> getTransformer(JpaQueryEnhancer.HqlQueryParser parser, QueryMethod method) {
9937
return new HqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(),
10038
method.getResultProcessor().getReturnedType());
10139
}
10240

103-
interface MyRepo extends Repository<Person, String> {
104-
105-
MyRecord dtoProjection();
106-
}
107-
108-
record Person(String id) {
109-
110-
}
111-
112-
record MyRecord(String foo, String bar) {
113-
114-
}
11541
}

0 commit comments

Comments
 (0)