Skip to content

Commit d75c58a

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 e69e3e5 commit d75c58a

File tree

4 files changed

+80
-15
lines changed

4 files changed

+80
-15
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: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.junit.jupiter.params.ParameterizedTest;
4343
import org.junit.jupiter.params.provider.Arguments;
4444
import org.junit.jupiter.params.provider.MethodSource;
45+
4546
import org.springframework.beans.factory.annotation.Autowired;
4647
import org.springframework.beans.factory.config.PropertiesFactoryBean;
4748
import org.springframework.context.ApplicationListener;
@@ -51,7 +52,16 @@
5152
import org.springframework.core.io.ClassPathResource;
5253
import org.springframework.dao.IncorrectResultSizeDataAccessException;
5354
import org.springframework.data.annotation.Id;
54-
import org.springframework.data.domain.*;
55+
import org.springframework.data.domain.Example;
56+
import org.springframework.data.domain.ExampleMatcher;
57+
import org.springframework.data.domain.Limit;
58+
import org.springframework.data.domain.Page;
59+
import org.springframework.data.domain.PageRequest;
60+
import org.springframework.data.domain.Pageable;
61+
import org.springframework.data.domain.ScrollPosition;
62+
import org.springframework.data.domain.Slice;
63+
import org.springframework.data.domain.Sort;
64+
import org.springframework.data.domain.Window;
5565
import org.springframework.data.jdbc.core.mapping.AggregateReference;
5666
import org.springframework.data.jdbc.repository.query.Modifying;
5767
import org.springframework.data.jdbc.repository.query.Query;
@@ -64,8 +74,8 @@
6474
import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
6575
import org.springframework.data.relational.core.mapping.Column;
6676
import org.springframework.data.relational.core.mapping.MappedCollection;
67-
import org.springframework.data.relational.core.mapping.Table;
6877
import org.springframework.data.relational.core.mapping.Sequence;
78+
import org.springframework.data.relational.core.mapping.Table;
6979
import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
7080
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
7181
import org.springframework.data.relational.core.sql.LockMode;
@@ -1187,6 +1197,32 @@ void fetchByExampleFluentCountSimple() {
11871197
assertThat(matches).isEqualTo(2);
11881198
}
11891199

1200+
@Test // GH-2098
1201+
void projectByExample() {
1202+
1203+
String searchName = "Diego";
1204+
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
1205+
1206+
DummyEntity entity = createEntity();
1207+
1208+
entity.setName(searchName);
1209+
entity.setPointInTime(now.minusSeconds(10000));
1210+
entity = repository.save(entity);
1211+
1212+
record DummyProjection(String name) {
1213+
1214+
}
1215+
1216+
Example<DummyEntity> example = Example.of(createEntity(searchName, it -> it.setBytes(null)));
1217+
1218+
DummyProjection projection = repository.findBy(example,
1219+
p -> p.project("name").as(DummyProjection.class).firstValue());
1220+
assertThat(projection.name()).isEqualTo(entity.name);
1221+
1222+
projection = repository.findBy(example, p -> p.project("flag").as(DummyProjection.class).firstValue());
1223+
assertThat(projection.name()).isNull();
1224+
}
1225+
11901226
@Test // GH-1192
11911227
void fetchByExampleFluentOnlyInstantFirstSimple() {
11921228

@@ -1888,10 +1924,10 @@ public String getName() {
18881924

18891925
static class DummyEntity {
18901926

1927+
@Id Long idProp;
18911928
String name;
18921929
Instant pointInTime;
18931930
OffsetDateTime offsetDateTime;
1894-
@Id private Long idProp;
18951931
boolean flag;
18961932
AggregateReference<DummyEntity, Long> ref;
18971933
Direction direction;

0 commit comments

Comments
 (0)