Browse Source

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
pull/2114/head
Mark Paluch 4 months ago
parent
commit
7ea58ddce4
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 15
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
  2. 28
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
  3. 4
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
  4. 28
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

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

@ -32,6 +32,7 @@ import org.springframework.data.domain.ScrollPosition; @@ -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<S, R> extends FluentQuerySupport<S, R> { @@ -47,19 +48,23 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
private final RelationalExampleMapper exampleMapper;
private final JdbcAggregateOperations entityOperations;
private final ProjectionFactory projectionFactory;
FetchableFluentQueryByExample(Example<S> example, Class<R> 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<S> example, Sort sort, int limit, Class<R> resultType,
List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
List<String> 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<S, R> extends FluentQuerySupport<S, R> { @@ -167,6 +172,6 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
List<String> fieldsToInclude) {
return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
this.entityOperations);
this.entityOperations, this.projectionFactory);
}
}

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

@ -21,9 +21,12 @@ import java.util.List; @@ -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<S, R> implements FluentQuery.FetchableFluentQu @@ -41,16 +44,19 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
private final int limit;
private final Class<R> resultType;
private final List<String> fieldsToInclude;
private final ProjectionFactory projectionFactory;
private final RelationalConverter converter;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude) {
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> 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<S, R> implements FluentQuery.FetchableFluentQu @@ -118,8 +124,18 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
return (Function<Object, R>) 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);

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

@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort; @@ -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; @@ -48,6 +49,7 @@ import org.springframework.util.Assert;
public class SimpleJdbcRepository<T, ID>
implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
private final JdbcAggregateOperations entityOperations;
private final PersistentEntity<T, ?> entity;
private final RelationalExampleMapper exampleMapper;
@ -197,7 +199,7 @@ public class SimpleJdbcRepository<T, ID> @@ -197,7 +199,7 @@ public class SimpleJdbcRepository<T, ID>
Assert.notNull(queryFunction, "Query function must not be null");
FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
example.getProbeType(), this.exampleMapper, this.entityOperations);
example.getProbeType(), this.exampleMapper, this.entityOperations, this.projectionFactory);
return queryFunction.apply(fluentQuery);
}

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

@ -1236,6 +1236,32 @@ public class JdbcRepositoryIntegrationTests { @@ -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<DummyEntity> 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 { @@ -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 { @@ -2012,7 +2039,6 @@ public class JdbcRepositoryIntegrationTests {
AggregateReference<DummyEntity, Long> 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;

Loading…
Cancel
Save