From 7ea58ddce42036ad356b30c1efdbd4ef0f51fb68 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Aug 2025 14:08:32 +0200 Subject: [PATCH] Apply DTO projection through JDBC's Query by Example. Spring Data JDBC doesn't allow projections through JdbcAggregateOperations yet and so we need to apply DTO conversion. Closes #2098 --- .../FetchableFluentQueryByExample.java | 15 ++++++---- .../support/FluentQuerySupport.java | 28 +++++++++++++++---- .../support/SimpleJdbcRepository.java | 4 ++- .../JdbcRepositoryIntegrationTests.java | 28 ++++++++++++++++++- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java index fe07f0758..7b12af819 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java @@ -32,6 +32,7 @@ import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.jdbc.core.JdbcAggregateOperations; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.repository.query.RelationalExampleMapper; import org.springframework.util.Assert; @@ -47,19 +48,23 @@ class FetchableFluentQueryByExample extends FluentQuerySupport { private final RelationalExampleMapper exampleMapper; private final JdbcAggregateOperations entityOperations; + private final ProjectionFactory projectionFactory; FetchableFluentQueryByExample(Example example, Class resultType, RelationalExampleMapper exampleMapper, - JdbcAggregateOperations entityOperations) { - this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations); + JdbcAggregateOperations entityOperations, ProjectionFactory projectionFactory) { + this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations, + projectionFactory); } FetchableFluentQueryByExample(Example example, Sort sort, int limit, Class resultType, - List fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) { + List fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations, + ProjectionFactory projectionFactory) { - super(example, sort, limit, resultType, fieldsToInclude); + super(example, sort, limit, resultType, fieldsToInclude, projectionFactory, entityOperations.getConverter()); this.exampleMapper = exampleMapper; this.entityOperations = entityOperations; + this.projectionFactory = projectionFactory; } @Override @@ -167,6 +172,6 @@ class FetchableFluentQueryByExample extends FluentQuerySupport { List fieldsToInclude) { return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper, - this.entityOperations); + this.entityOperations, this.projectionFactory); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java index 1b96b3c66..1c3ea5de0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java @@ -21,9 +21,12 @@ import java.util.List; import java.util.function.Function; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.convert.DtoInstantiatingConverter; import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.repository.query.FluentQuery; import org.springframework.util.Assert; @@ -41,16 +44,19 @@ abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQu private final int limit; private final Class resultType; private final List fieldsToInclude; + private final ProjectionFactory projectionFactory; + private final RelationalConverter converter; - private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); - - FluentQuerySupport(Example example, Sort sort, int limit, Class resultType, List fieldsToInclude) { + FluentQuerySupport(Example example, Sort sort, int limit, Class resultType, List fieldsToInclude, + ProjectionFactory projectionFactory, RelationalConverter converter) { this.example = example; this.sort = sort; this.limit = limit; this.resultType = resultType; this.fieldsToInclude = fieldsToInclude; + this.projectionFactory = projectionFactory; + this.converter = converter; } @Override @@ -118,8 +124,18 @@ abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQu return (Function) Function.identity(); } - if (targetType.isInterface()) { - return o -> projectionFactory.createProjection(targetType, o); + EntityProjection entityProjection = converter.introspectProjection(targetType, inputType); + + if (entityProjection.isProjection()) { + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + DtoInstantiatingConverter dtoConverter = new DtoInstantiatingConverter(targetType, converter.getMappingContext(), + converter.getEntityInstantiators()); + + return o -> (R) dtoConverter.convert(o); } return o -> DefaultConversionService.getSharedInstance().convert(o, targetType); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index 65eba4b02..585755541 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java @@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.repository.query.RelationalExampleMapper; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; @@ -48,6 +49,7 @@ import org.springframework.util.Assert; public class SimpleJdbcRepository implements CrudRepository, PagingAndSortingRepository, QueryByExampleExecutor { + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); private final JdbcAggregateOperations entityOperations; private final PersistentEntity entity; private final RelationalExampleMapper exampleMapper; @@ -197,7 +199,7 @@ public class SimpleJdbcRepository Assert.notNull(queryFunction, "Query function must not be null"); FluentQuery.FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, - example.getProbeType(), this.exampleMapper, this.entityOperations); + example.getProbeType(), this.exampleMapper, this.entityOperations, this.projectionFactory); return queryFunction.apply(fluentQuery); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 52200a02f..10bd3eced 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -1236,6 +1236,32 @@ public class JdbcRepositoryIntegrationTests { assertThat(matches).isEqualTo(2); } + @Test // GH-2098 + void projectByExample() { + + String searchName = "Diego"; + Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + DummyEntity entity = createEntity(); + + entity.setName(searchName); + entity.setPointInTime(now.minusSeconds(10000)); + entity = repository.save(entity); + + record DummyProjection(String name) { + + } + + Example example = Example.of(createEntity(searchName, it -> it.setBytes(null))); + + DummyProjection projection = repository.findBy(example, + p -> p.project("name").as(DummyProjection.class).firstValue()); + assertThat(projection.name()).isEqualTo(entity.name); + + projection = repository.findBy(example, p -> p.project("flag").as(DummyProjection.class).firstValue()); + assertThat(projection.name()).isNull(); + } + @Test // GH-1192 void fetchByExampleFluentOnlyInstantFirstSimple() { @@ -2005,6 +2031,7 @@ public class JdbcRepositoryIntegrationTests { static class DummyEntity { + @Id Long idProp; String name; Instant pointInTime; OffsetDateTime offsetDateTime; @@ -2012,7 +2039,6 @@ public class JdbcRepositoryIntegrationTests { AggregateReference ref; Direction direction; byte[] bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; - @Id private Long idProp; public DummyEntity(String name) { this.name = name;