Skip to content

Commit 7ea58dd

Browse files
committed
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
1 parent e413637 commit 7ea58dd

File tree

4 files changed

+62
-13
lines changed

4 files changed

+62
-13
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.domain.Sort;
3333
import org.springframework.data.domain.Window;
3434
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
35+
import org.springframework.data.projection.ProjectionFactory;
3536
import org.springframework.data.relational.core.query.Query;
3637
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
3738
import org.springframework.util.Assert;
@@ -47,19 +48,23 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
4748

4849
private final RelationalExampleMapper exampleMapper;
4950
private final JdbcAggregateOperations entityOperations;
51+
private final ProjectionFactory projectionFactory;
5052

5153
FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper,
52-
JdbcAggregateOperations entityOperations) {
53-
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations);
54+
JdbcAggregateOperations entityOperations, ProjectionFactory projectionFactory) {
55+
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations,
56+
projectionFactory);
5457
}
5558

5659
FetchableFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<R> resultType,
57-
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
60+
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations,
61+
ProjectionFactory projectionFactory) {
5862

59-
super(example, sort, limit, resultType, fieldsToInclude);
63+
super(example, sort, limit, resultType, fieldsToInclude, projectionFactory, entityOperations.getConverter());
6064

6165
this.exampleMapper = exampleMapper;
6266
this.entityOperations = entityOperations;
67+
this.projectionFactory = projectionFactory;
6368
}
6469

6570
@Override
@@ -167,6 +172,6 @@ protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, int
167172
List<String> fieldsToInclude) {
168173

169174
return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
170-
this.entityOperations);
175+
this.entityOperations, this.projectionFactory);
171176
}
172177
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.util.function.Function;
2222

2323
import org.springframework.core.convert.support.DefaultConversionService;
24+
import org.springframework.data.convert.DtoInstantiatingConverter;
2425
import org.springframework.data.domain.Example;
2526
import org.springframework.data.domain.Sort;
26-
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
27+
import org.springframework.data.projection.EntityProjection;
28+
import org.springframework.data.projection.ProjectionFactory;
29+
import org.springframework.data.relational.core.conversion.RelationalConverter;
2730
import org.springframework.data.repository.query.FluentQuery;
2831
import org.springframework.util.Assert;
2932

@@ -41,16 +44,19 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
4144
private final int limit;
4245
private final Class<R> resultType;
4346
private final List<String> fieldsToInclude;
47+
private final ProjectionFactory projectionFactory;
48+
private final RelationalConverter converter;
4449

45-
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
46-
47-
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude) {
50+
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude,
51+
ProjectionFactory projectionFactory, RelationalConverter converter) {
4852

4953
this.example = example;
5054
this.sort = sort;
5155
this.limit = limit;
5256
this.resultType = resultType;
5357
this.fieldsToInclude = fieldsToInclude;
58+
this.projectionFactory = projectionFactory;
59+
this.converter = converter;
5460
}
5561

5662
@Override
@@ -118,8 +124,18 @@ private Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> t
118124
return (Function<Object, R>) Function.identity();
119125
}
120126

121-
if (targetType.isInterface()) {
122-
return o -> projectionFactory.createProjection(targetType, o);
127+
EntityProjection<?, ?> entityProjection = converter.introspectProjection(targetType, inputType);
128+
129+
if (entityProjection.isProjection()) {
130+
131+
if (targetType.isInterface()) {
132+
return o -> projectionFactory.createProjection(targetType, o);
133+
}
134+
135+
DtoInstantiatingConverter dtoConverter = new DtoInstantiatingConverter(targetType, converter.getMappingContext(),
136+
converter.getEntityInstantiators());
137+
138+
return o -> (R) dtoConverter.convert(o);
123139
}
124140

125141
return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
2727
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2828
import org.springframework.data.mapping.PersistentEntity;
29+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
2930
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
3031
import org.springframework.data.repository.CrudRepository;
3132
import org.springframework.data.repository.PagingAndSortingRepository;
@@ -48,6 +49,7 @@
4849
public class SimpleJdbcRepository<T, ID>
4950
implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
5051

52+
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
5153
private final JdbcAggregateOperations entityOperations;
5254
private final PersistentEntity<T, ?> entity;
5355
private final RelationalExampleMapper exampleMapper;
@@ -197,7 +199,7 @@ public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.Fetcha
197199
Assert.notNull(queryFunction, "Query function must not be null");
198200

199201
FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
200-
example.getProbeType(), this.exampleMapper, this.entityOperations);
202+
example.getProbeType(), this.exampleMapper, this.entityOperations, this.projectionFactory);
201203

202204
return queryFunction.apply(fluentQuery);
203205
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,32 @@ void fetchByExampleFluentCountSimple() {
12361236
assertThat(matches).isEqualTo(2);
12371237
}
12381238

1239+
@Test // GH-2098
1240+
void projectByExample() {
1241+
1242+
String searchName = "Diego";
1243+
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
1244+
1245+
DummyEntity entity = createEntity();
1246+
1247+
entity.setName(searchName);
1248+
entity.setPointInTime(now.minusSeconds(10000));
1249+
entity = repository.save(entity);
1250+
1251+
record DummyProjection(String name) {
1252+
1253+
}
1254+
1255+
Example<DummyEntity> example = Example.of(createEntity(searchName, it -> it.setBytes(null)));
1256+
1257+
DummyProjection projection = repository.findBy(example,
1258+
p -> p.project("name").as(DummyProjection.class).firstValue());
1259+
assertThat(projection.name()).isEqualTo(entity.name);
1260+
1261+
projection = repository.findBy(example, p -> p.project("flag").as(DummyProjection.class).firstValue());
1262+
assertThat(projection.name()).isNull();
1263+
}
1264+
12391265
@Test // GH-1192
12401266
void fetchByExampleFluentOnlyInstantFirstSimple() {
12411267

@@ -2005,14 +2031,14 @@ public boolean isNew() {
20052031

20062032
static class DummyEntity {
20072033

2034+
@Id Long idProp;
20082035
String name;
20092036
Instant pointInTime;
20102037
OffsetDateTime offsetDateTime;
20112038
boolean flag;
20122039
AggregateReference<DummyEntity, Long> ref;
20132040
Direction direction;
20142041
byte[] bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
2015-
@Id private Long idProp;
20162042

20172043
public DummyEntity(String name) {
20182044
this.name = name;

0 commit comments

Comments
 (0)