Skip to content

Commit 6b83dca

Browse files
committed
DTO, Interface, and Dynamic Projections.
1 parent d9dffb9 commit 6b83dca

File tree

8 files changed

+165
-80
lines changed

8 files changed

+165
-80
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/aot/AotRepositoryFragmentSupport.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.core.convert.support.DefaultConversionService;
3131
import org.springframework.dao.DataAccessException;
3232
import org.springframework.dao.support.DataAccessUtils;
33+
import org.springframework.data.convert.DtoInstantiatingConverter;
3334
import org.springframework.data.domain.Slice;
3435
import org.springframework.data.expression.ValueEvaluationContext;
3536
import org.springframework.data.expression.ValueEvaluationContextProvider;
@@ -45,6 +46,7 @@
4546
import org.springframework.data.jdbc.support.JdbcUtil;
4647
import org.springframework.data.projection.ProjectionFactory;
4748
import org.springframework.data.relational.core.dialect.Dialect;
49+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
4850
import org.springframework.data.repository.core.RepositoryMetadata;
4951
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
5052
import org.springframework.data.repository.query.ParametersSource;
@@ -215,6 +217,14 @@ private static SQLType getSqlType(Class<?> valueType) {
215217
return CONVERSION_SERVICE.convert(result, projection);
216218
}
217219

220+
if (!projection.isInterface()) {
221+
222+
RelationalMappingContext mappingContext = aggregateOperations.getConverter().getMappingContext();
223+
DtoInstantiatingConverter converter = new DtoInstantiatingConverter(projection, mappingContext,
224+
aggregateOperations.getConverter().getEntityInstantiators());
225+
return (T) converter.convert(result);
226+
}
227+
218228
return projectionFactory.createProjection(projection, result);
219229
}
220230

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/aot/JdbcCodeBlocks.java

Lines changed: 24 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,6 @@ public CodeBlock build() {
573573
return delete(builder, rowMapper, result, queryResultType, returnType, actualReturnType);
574574
} else {
575575

576-
// TODO: Projection
577576
String resultSetExtractor = null;
578577

579578
if (rowMapperClass != null) {
@@ -583,8 +582,17 @@ public CodeBlock build() {
583582
builder.addStatement("$T $L = $L.getRowMapper($S)", RowMapper.class, rowMapper,
584583
context.fieldNameOf(RowMapperFactory.class), rowMapperRef);
585584
} else if (resultSetExtractorClass == null) {
585+
586+
Type typeToRead;
587+
588+
if (isProjecting) {
589+
typeToRead = context.getReturnedType().getDomainType();
590+
} else {
591+
typeToRead = context.getActualReturnType().getType();
592+
}
593+
586594
builder.addStatement("$T $L = $L.create($T.class)", RowMapper.class, rowMapper,
587-
context.fieldNameOf(RowMapperFactory.class), actualReturnType);
595+
context.fieldNameOf(RowMapperFactory.class), typeToRead);
588596
}
589597

590598
if (StringUtils.hasText(resultSetExtractorRef) || resultSetExtractorClass != null) {
@@ -611,6 +619,9 @@ public CodeBlock build() {
611619
return builder.build();
612620
}
613621

622+
boolean dynamicProjection = StringUtils.hasText(context.getDynamicProjectionParameterName());
623+
Object queryResultTypeRef = dynamicProjection ? context.getDynamicProjectionParameterName() : queryResultType;
624+
614625
if (queryMethod.isCollectionQuery() || queryMethod.isSliceQuery() || queryMethod.isPageQuery()) {
615626

616627
builder.addStatement("$1T $2L = ($1T) getJdbcOperations().query($3L, $4L, new $5T<>($6L))", List.class,
@@ -620,8 +631,9 @@ public CodeBlock build() {
620631

621632
String pageable = context.getPageableParameterName();
622633

623-
builder.addStatement("$1T $2L = ($1T) convertMany($3L, $4T.class)", List.class,
624-
context.localVariable("converted"), result, queryResultType);
634+
builder.addStatement(
635+
"$1T $2L = ($1T) convertMany($3L, %s)".formatted(dynamicProjection ? "$4L" : "$4T.class"), List.class,
636+
context.localVariable("converted"), result, queryResultTypeRef);
625637

626638
if (queryMethod.isPageQuery()) {
627639

@@ -639,94 +651,28 @@ public CodeBlock build() {
639651
return builder.build();
640652
}
641653

642-
builder.addStatement("return ($T) convertMany($L, $T.class)", context.getReturnTypeName(), result,
643-
queryResultType);
654+
builder.addStatement("return ($T) convertMany($L, %s)".formatted(dynamicProjection ? "$L" : "$T.class"),
655+
context.getReturnTypeName(), result, queryResultTypeRef);
644656
} else if (queryMethod.isStreamQuery()) {
645657

646658
builder.addStatement("$1T $2L = getJdbcOperations().queryForStream($3L, $4L, $5L)", Stream.class, result,
647659
queryVariableName, parameterSourceVariableName, rowMapper);
648660
builder.addStatement("return ($T) convertMany($L, $T.class)", context.getReturnTypeName(), result,
649-
queryResultType);
661+
queryResultTypeRef);
650662
} else {
651663

652664
builder.addStatement("$T $L = queryForObject($L, $L, $L)", Object.class, result, queryVariableName,
653665
parameterSourceVariableName, rowMapper);
654666

655667
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
656-
builder.addStatement("return ($1T) $1T.ofNullable(convertOne($2L, $3T.class))", Optional.class, result,
657-
queryResultType);
668+
builder.addStatement(
669+
"return ($1T) $1T.ofNullable(convertOne($2L, %s))".formatted(dynamicProjection ? "$3L" : "$3T.class"),
670+
Optional.class, result, queryResultTypeRef);
658671
} else {
659-
builder.addStatement("return ($T) convertOne($L, $T.class)", context.getReturnTypeName(), result,
660-
queryResultType);
672+
builder.addStatement("return ($T) convertOne($L, %s)".formatted(dynamicProjection ? "$L" : "$T.class"),
673+
context.getReturnTypeName(), result, queryResultTypeRef);
661674
}
662675
}
663-
664-
/* if (context.getReturnedType().isProjecting()) {
665-
666-
if (queryMethod.isCollectionQuery()) {
667-
builder.addStatement("return ($T) convertMany($L.getResultList(), $L, $T.class)",
668-
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType);
669-
} else if (queryMethod.isStreamQuery()) {
670-
builder.addStatement("return ($T) convertMany($L.getResultStream(), $L, $T.class)",
671-
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType);
672-
} else if (queryMethod.isPageQuery()) {
673-
builder.addStatement("return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, $L)",
674-
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(),
675-
queryResultType, pageable, context.localVariable("countAll"));
676-
} else if (queryMethod.isSliceQuery()) {
677-
builder.addStatement("$T<$T> $L = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)", List.class,
678-
actualReturnType, context.localVariable("resultList"), List.class, actualReturnType, queryVariableName,
679-
aotQuery.isNative(), queryResultType);
680-
builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()",
681-
context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable);
682-
builder.addStatement("return new $T<>($L ? $L.subList(0, $L.getPageSize()) : $L, $L, $L)", SliceImpl.class,
683-
context.localVariable("hasNext"), context.localVariable("resultList"), pageable,
684-
context.localVariable("resultList"), pageable, context.localVariable("hasNext"));
685-
} else {
686-
687-
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
688-
builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))",
689-
Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(), queryResultType);
690-
} else {
691-
builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)",
692-
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType);
693-
}
694-
}
695-
696-
} else {
697-
698-
if(resultSetExtractor != null){
699-
700-
}
701-
702-
if (queryMethod.isCollectionQuery()) {
703-
builder.addStatement("return ($T) $L.getResultList()", context.getReturnTypeName(), queryVariableName);
704-
} else if (queryMethod.isStreamQuery()) {
705-
builder.addStatement("return ($T) $L.getResultStream()", context.getReturnTypeName(), queryVariableName);
706-
} else if (queryMethod.isPageQuery()) {
707-
builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, $L)", PageableExecutionUtils.class,
708-
List.class, actualReturnType, queryVariableName, pageable, context.localVariable("countAll"));
709-
} else if (queryMethod.isSliceQuery()) {
710-
builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, actualReturnType,
711-
context.localVariable("resultList"), queryVariableName);
712-
builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()",
713-
context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable);
714-
builder.addStatement("return new $T<>($L ? $L.subList(0, $L.getPageSize()) : $L, $L, $L)", SliceImpl.class,
715-
context.localVariable("hasNext"), context.localVariable("resultList"), pageable,
716-
context.localVariable("resultList"), pageable, context.localVariable("hasNext"));
717-
} else {
718-
719-
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
720-
builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))",
721-
Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(),
722-
context.getActualReturnType().toClass());
723-
} else {
724-
builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)",
725-
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(),
726-
context.getReturnType().toClass());
727-
}
728-
}
729-
} */
730676
}
731677

732678
return builder.build();

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StatementFactory.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.data.relational.core.sql.SelectBuilder;
4141
import org.springframework.data.relational.core.sql.Table;
4242
import org.springframework.data.relational.core.sql.render.SqlRenderer;
43+
import org.springframework.data.util.Predicates;
4344
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
4445
import org.springframework.lang.Nullable;
4546

@@ -232,8 +233,13 @@ SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?>
232233

233234
private SelectBuilder.SelectJoin selectBuilder(Table table) {
234235

235-
Predicate<AggregatePath> filter = ap -> !properties.isEmpty()
236-
&& !properties.contains(ap.getRequiredBaseProperty().getName());
236+
Predicate<AggregatePath> filter;
237+
238+
if (properties.isEmpty()) {
239+
filter = Predicates.isFalse();
240+
} else {
241+
filter = ap -> !properties.contains(ap.getRequiredBaseProperty().getName());
242+
}
237243

238244
return (SelectBuilder.SelectJoin) sqlGeneratorSource.getSqlGenerator(entity.getType()).createSelectBuilder(table,
239245
filter);

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,50 @@ void shouldFindUsingResultSetExtractorRef() {
272272
assertThat(result).isOne();
273273
}
274274

275+
@Test // GH-2121
276+
void shouldProjectOneToDto() {
277+
278+
UserDto dto = fragment.findOneDtoByFirstname("Walter");
279+
280+
assertThat(dto).isNotNull();
281+
assertThat(dto.firstname()).isEqualTo("Walter");
282+
}
283+
284+
@Test // GH-2121
285+
void shouldProjectListToDto() {
286+
287+
List<UserDto> dtos = fragment.findDtoByFirstname("Walter");
288+
289+
assertThat(dtos).hasSize(1).extracting(UserDto::firstname).containsOnly("Walter");
290+
}
291+
292+
@Test // GH-2121
293+
void shouldProjectOneToInterface() {
294+
295+
UserProjection projection = fragment.findOneInterfaceByFirstname("Walter");
296+
297+
assertThat(projection).isNotNull();
298+
assertThat(projection.getFirstname()).isEqualTo("Walter");
299+
}
300+
301+
@Test // GH-2121
302+
void shouldProjectListToInterface() {
303+
304+
List<UserProjection> projections = fragment.findInterfaceByFirstname("Walter");
305+
306+
assertThat(projections).hasSize(1).extracting(UserProjection::getFirstname).containsOnly("Walter");
307+
}
308+
309+
@Test // GH-2121
310+
void shouldProjectDynamically() {
311+
312+
List<UserDto> dtos = fragment.findDynamicProjectionByFirstname("Walter", UserDto.class);
313+
assertThat(dtos).hasSize(1).extracting(UserDto::firstname).containsOnly("Walter");
314+
315+
List<UserProjection> projections = fragment.findDynamicProjectionByFirstname("Walter", UserProjection.class);
316+
assertThat(projections).hasSize(1).extracting(UserProjection::getFirstname).containsOnly("Walter");
317+
}
318+
275319
@Test // GH-2121
276320
void shouldDeleteByName() {
277321

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,21 @@ void shouldDocumentExplicitlyNamedQuery() throws IOException {
211211
.containsEntry("query", "SELECT ANNOTATED FROM USER WHERE NAME = :name");
212212
}
213213

214+
@Test // GH-2121
215+
void shouldDocumentInterfaceProjection() throws IOException {
216+
217+
Resource resource = getResource();
218+
219+
assertThat(resource).isNotNull();
220+
assertThat(resource.exists()).isTrue();
221+
222+
String json = resource.getContentAsString(StandardCharsets.UTF_8);
223+
224+
assertThatJson(json).inPath("$.methods[?(@.name == 'findInterfaceByFirstname')].query").isArray().first().isObject()
225+
.containsEntry("query",
226+
"SELECT \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" = :firstname");
227+
}
228+
214229
@Test // GH-2121
215230
void shouldDocumentBaseFragment() throws IOException {
216231

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.jdbc.repository.aot;
17+
18+
/**
19+
* @author Mark Paluch
20+
*/
21+
public record UserDto(String firstname) {
22+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.jdbc.repository.aot;
17+
18+
/**
19+
* @author Mark Paluch
20+
*/
21+
public interface UserProjection {
22+
23+
String getFirstname();
24+
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/UserRepository.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,24 @@ List<User> findWithParameterNameByFirstnameStartingWithOrFirstnameEndingWith(@Pa
105105
@Query(name = "User.findBySomeAnnotatedNamedQuery")
106106
User findByAnnotatedNamedQuery(String name);
107107

108+
// -------------------------------------------------------------------------
109+
// Projections: DTO
110+
// -------------------------------------------------------------------------
111+
112+
UserDto findOneDtoByFirstname(String name);
113+
114+
List<UserDto> findDtoByFirstname(String name);
115+
116+
// -------------------------------------------------------------------------
117+
// Projections: Interface
118+
// -------------------------------------------------------------------------
119+
120+
UserProjection findOneInterfaceByFirstname(String name);
121+
122+
List<UserProjection> findInterfaceByFirstname(String name);
123+
124+
<T> List<T> findDynamicProjectionByFirstname(String name, Class<T> type);
125+
108126
// -------------------------------------------------------------------------
109127
// Modifying
110128
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)