Skip to content

Commit 80db3d8

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 6ae2380 commit 80db3d8

File tree

4 files changed

+65
-13
lines changed

4 files changed

+65
-13
lines changed

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
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;
36+
import org.springframework.data.relational.core.conversion.RelationalConverter;
3537
import org.springframework.data.relational.core.query.Query;
3638
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
3739
import org.springframework.util.Assert;
@@ -47,19 +49,26 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
4749

4850
private final RelationalExampleMapper exampleMapper;
4951
private final JdbcAggregateOperations entityOperations;
52+
private final ProjectionFactory projectionFactory;
53+
private final RelationalConverter converter;
5054

5155
FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper,
52-
JdbcAggregateOperations entityOperations) {
53-
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations);
56+
JdbcAggregateOperations entityOperations, RelationalConverter converter, ProjectionFactory projectionFactory) {
57+
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations, converter,
58+
projectionFactory);
5459
}
5560

5661
FetchableFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<R> resultType,
57-
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
62+
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations,
63+
RelationalConverter converter,
64+
ProjectionFactory projectionFactory) {
5865

59-
super(example, sort, limit, resultType, fieldsToInclude);
66+
super(example, sort, limit, resultType, fieldsToInclude, projectionFactory, converter);
6067

6168
this.exampleMapper = exampleMapper;
6269
this.entityOperations = entityOperations;
70+
this.converter = converter;
71+
this.projectionFactory = projectionFactory;
6372
}
6473

6574
@Override
@@ -167,6 +176,6 @@ protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, int
167176
List<String> fieldsToInclude) {
168177

169178
return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
170-
this.entityOperations);
179+
this.entityOperations, this.converter, this.projectionFactory);
171180
}
172181
}

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: 5 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,9 +49,11 @@
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;
56+
private final JdbcConverter converter;
5457

5558
public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity<T, ?> entity,
5659
JdbcConverter converter) {
@@ -60,6 +63,7 @@ public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, Persistent
6063

6164
this.entityOperations = entityOperations;
6265
this.entity = entity;
66+
this.converter = converter;
6367
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
6468
}
6569

@@ -197,7 +201,7 @@ public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.Fetcha
197201
Assert.notNull(queryFunction, "Query function must not be null");
198202

199203
FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
200-
example.getProbeType(), this.exampleMapper, this.entityOperations);
204+
example.getProbeType(), this.exampleMapper, this.entityOperations, this.converter, this.projectionFactory);
201205

202206
return queryFunction.apply(fluentQuery);
203207
}

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,29 @@ void fetchByExampleFluentCountSimple() {
11721172
assertThat(matches).isEqualTo(2);
11731173
}
11741174

1175+
@Test // GH-2098
1176+
void projectByExample() {
1177+
1178+
String searchName = "Diego";
1179+
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
1180+
1181+
DummyEntity entity = createEntity();
1182+
1183+
entity.setName(searchName);
1184+
entity.setPointInTime(now.minusSeconds(10000));
1185+
entity = repository.save(entity);
1186+
1187+
record DummyProjection(String name) {
1188+
1189+
}
1190+
1191+
Example<DummyEntity> example = Example.of(createEntity(searchName, it -> it.setBytes(null)));
1192+
1193+
DummyProjection projection = repository.findBy(example,
1194+
p -> p.project("name").as(DummyProjection.class).firstValue());
1195+
assertThat(projection.name()).isEqualTo(entity.name);
1196+
}
1197+
11751198
@Test // GH-1192
11761199
void fetchByExampleFluentOnlyInstantFirstSimple() {
11771200

@@ -1841,10 +1864,10 @@ private static DummyEntity createEntity(String entityName, Consumer<DummyEntity>
18411864

18421865
static class DummyEntity {
18431866

1867+
@Id Long idProp;
18441868
String name;
18451869
Instant pointInTime;
18461870
OffsetDateTime offsetDateTime;
1847-
@Id private Long idProp;
18481871
boolean flag;
18491872
AggregateReference<DummyEntity, Long> ref;
18501873
Direction direction;

0 commit comments

Comments
 (0)