Skip to content

Commit 78d350b

Browse files
committed
Exclude DTO types without custom construction from DTO constructor rewriting.
We now verify that we can actually express a valid constructor expression before rewriting queries to use constructor expressions. See #3929
1 parent fa30902 commit 78d350b

File tree

11 files changed

+247
-5
lines changed

11 files changed

+247
-5
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
157157
ReturnedType getReturnedType(ResultProcessor processor) {
158158

159159
ReturnedType returnedType = processor.getReturnedType();
160-
Class<?> returnedJavaType = processor.getReturnedType().getReturnedType();
160+
Class<?> returnedJavaType = returnedType.getReturnedType();
161161

162162
if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNative()) {
163163
return returnedType;
@@ -169,7 +169,8 @@ ReturnedType getReturnedType(ResultProcessor processor) {
169169
return returnedType;
170170
}
171171

172-
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) {
172+
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)
173+
|| !returnedType.needsCustomConstruction()) {
173174
if (known == null) {
174175
knownProjections.put(returnedJavaType, false);
175176
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 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.domain.sample;
17+
18+
/**
19+
* @author Mark Paluch
20+
*/
21+
public class Country {
22+
23+
private final String code;
24+
25+
// workaround to avoid DTO projections as needsCustomConstruction is false.
26+
private Country(Country other) {
27+
this.code = other.code;
28+
}
29+
30+
private Country(String code) {
31+
this.code = code;
32+
}
33+
34+
public static Country of(String code) {
35+
return new Country(code);
36+
}
37+
38+
public String getCode() {
39+
return code;
40+
}
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 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.domain.sample;
17+
18+
import jakarta.persistence.AttributeConverter;
19+
import jakarta.persistence.Converter;
20+
21+
@Converter(autoApply = true)
22+
public class CountryConverter implements AttributeConverter<Country, String> {
23+
24+
@Override
25+
public String convertToDatabaseColumn(Country attribute) {
26+
return attribute.getCode();
27+
}
28+
29+
@Override
30+
public Country convertToEntityAttribute(String dbData) {
31+
return Country.of(dbData);
32+
}
33+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,45 @@
1515
*/
1616
package org.springframework.data.jpa.domain.sample;
1717

18+
import jakarta.persistence.Convert;
1819
import jakarta.persistence.Entity;
1920
import jakarta.persistence.Id;
2021

2122
/**
2223
* @author Oliver Gierke
2324
* @author Patrice Blanchardie
25+
* @author Mark Paluch
2426
*/
2527
@Entity
2628
public class Customer {
2729

28-
@Id Long id;
30+
@Id Long id;
2931

30-
String name;
32+
String name;
33+
34+
@Convert(converter = CountryConverter.class) Country country;
35+
36+
public Long getId() {
37+
return id;
38+
}
39+
40+
public void setId(Long id) {
41+
this.id = id;
42+
}
43+
44+
public String getName() {
45+
return name;
46+
}
47+
48+
public void setName(String name) {
49+
this.name = name;
50+
}
51+
52+
public Country getCountry() {
53+
return country;
54+
}
55+
56+
public void setCountry(Country country) {
57+
this.country = country;
58+
}
3159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2008-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;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import java.util.List;
21+
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.data.jpa.domain.sample.Country;
28+
import org.springframework.data.jpa.domain.sample.Customer;
29+
import org.springframework.data.jpa.repository.sample.CustomerRepository;
30+
import org.springframework.test.context.ContextConfiguration;
31+
import org.springframework.test.context.junit.jupiter.SpringExtension;
32+
import org.springframework.transaction.annotation.Transactional;
33+
34+
/**
35+
* Integration test for executing projecting query methods.
36+
*
37+
* @author Mark Paluch
38+
*/
39+
@ExtendWith(SpringExtension.class)
40+
@ContextConfiguration(locations = "classpath:config/namespace-application-context-h2.xml")
41+
@Transactional
42+
class CustomerRepositoryProjectionTests {
43+
44+
@Autowired CustomerRepository repository;
45+
46+
@AfterEach
47+
void clearUp() {
48+
repository.deleteAll();
49+
}
50+
51+
@Test
52+
void returnsCountries() {
53+
54+
Customer customer = new Customer();
55+
customer.setId(42L);
56+
customer.setCountry(Country.of("DE"));
57+
customer.setName("someone");
58+
59+
repository.saveAndFlush(customer);
60+
61+
List<Country> countries = repository.findCountries();
62+
63+
assertThat(countries).hasSize(1);
64+
}
65+
66+
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.repository;
1717

18-
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.*;
1919

2020
import jakarta.persistence.EntityManager;
2121

@@ -25,6 +25,7 @@
2525

2626
import org.junit.jupiter.api.Test;
2727
import org.junit.jupiter.api.extension.ExtendWith;
28+
2829
import org.springframework.beans.factory.annotation.Autowired;
2930
import org.springframework.data.domain.Page;
3031
import org.springframework.data.domain.PageRequest;
@@ -115,6 +116,24 @@ void shouldSupportSavingEntitiesWithCompositeKeyClassesWithEmbeddedIdsAndDerived
115116
assertThat(persistedEmp.getDepartment().getName()).isEqualTo(dep.getName());
116117
}
117118

119+
@Test // GH-3929
120+
void shouldReturnIdentifiers() {
121+
122+
EmbeddedIdExampleDepartment dep = new EmbeddedIdExampleDepartment();
123+
dep.setName("TestDepartment");
124+
dep.setDepartmentId(-1L);
125+
126+
EmbeddedIdExampleEmployee emp = new EmbeddedIdExampleEmployee();
127+
emp.setDepartment(dep);
128+
emp.setEmployeePk(new EmbeddedIdExampleEmployeePK(1L, 2L));
129+
130+
emp = employeeRepositoryWithEmbeddedId.save(emp);
131+
132+
List<EmbeddedIdExampleEmployeePK> identifiers = employeeRepositoryWithEmbeddedId.findIdentifiers();
133+
134+
assertThat(identifiers).hasSize(1).contains(emp.getEmployeePk());
135+
}
136+
118137
@Test // DATAJPA-472, DATAJPA-912
119138
void shouldSupportFindAllWithPageableAndEntityWithIdClass() {
120139

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.data.domain.PageRequest;
4747
import org.springframework.data.domain.Pageable;
4848
import org.springframework.data.domain.Sort;
49+
import org.springframework.data.jpa.domain.sample.Country;
4950
import org.springframework.data.jpa.domain.sample.User;
5051
import org.springframework.data.jpa.provider.QueryExtractor;
5152
import org.springframework.data.jpa.repository.NativeQuery;
@@ -325,6 +326,17 @@ void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception {
325326
assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(illegalMethod));
326327
}
327328

329+
@Test // GH-3929
330+
void doesNotRewriteQueryForDtoWithMultipleConstructors() throws Exception {
331+
332+
AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery(
333+
SampleRepository.class.getMethod("justCountries"));
334+
335+
String queryString = createQuery(jpaQuery);
336+
337+
assertThat(queryString).startsWith("select u.country from User u");
338+
}
339+
328340
@Test // DATAJPA-1163
329341
void resolvesExpressionInCountQuery() throws Exception {
330342

@@ -408,6 +420,9 @@ interface SampleRepository extends Repository<User, Long> {
408420
@Query("select r.name from User u LEFT JOIN FETCH u.roles r")
409421
Collection<UnrelatedType> projectWithJoinPaths();
410422

423+
@Query("select u.country from User u")
424+
Collection<Country> justCountries();
425+
411426
@Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u")
412427
List<User> findAllWithExpressionInCountQuery(Pageable pageable);
413428

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 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.sample;
17+
18+
import java.util.List;
19+
20+
import org.springframework.data.jpa.domain.sample.Country;
21+
import org.springframework.data.jpa.domain.sample.Customer;
22+
import org.springframework.data.jpa.repository.JpaRepository;
23+
import org.springframework.data.jpa.repository.Query;
24+
25+
/**
26+
* @author Mark Paluch
27+
*/
28+
public interface CustomerRepository extends JpaRepository<Customer, Integer> {
29+
30+
@Query("SELECT c.country FROM Customer c")
31+
List<Country> findCountries();
32+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithEmbeddedId.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee;
2222
import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK;
2323
import org.springframework.data.jpa.repository.JpaRepository;
24+
import org.springframework.data.jpa.repository.Query;
2425
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
2526

2627
import com.querydsl.core.types.OrderSpecifier;
@@ -40,6 +41,9 @@ public interface EmployeeRepositoryWithEmbeddedId
4041
@Override
4142
List<EmbeddedIdExampleEmployee> findAll(Predicate predicate, OrderSpecifier<?>... orders);
4243

44+
@Query("select e.employeePk from EmbeddedIdExampleEmployee e")
45+
List<EmbeddedIdExampleEmployeePK> findIdentifiers();
46+
4347
// DATAJPA-920
4448
boolean existsByName(String name);
4549
}

spring-data-jpa/src/test/resources/META-INF/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<class>org.springframework.data.jpa.domain.sample.ConcreteType2</class>
2424
<class>org.springframework.data.jpa.domain.sample.CustomAbstractPersistable</class>
2525
<class>org.springframework.data.jpa.domain.sample.Customer</class>
26+
<class>org.springframework.data.jpa.domain.sample.CountryConverter</class>
2627
<class>org.springframework.data.jpa.domain.sample.EntityWithAssignedId</class>
2728
<class>org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK</class>
2829
<class>org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee</class>

0 commit comments

Comments
 (0)