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
3.5.x
Mark Paluch 4 months ago
parent
commit
d75c58a9f1
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 19
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
  2. 24
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
  3. 6
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
  4. 42
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

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

@ -32,6 +32,8 @@ import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window; import org.springframework.data.domain.Window;
import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.repository.query.RelationalExampleMapper; import org.springframework.data.relational.repository.query.RelationalExampleMapper;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -47,19 +49,26 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
private final RelationalExampleMapper exampleMapper; private final RelationalExampleMapper exampleMapper;
private final JdbcAggregateOperations entityOperations; private final JdbcAggregateOperations entityOperations;
private final ProjectionFactory projectionFactory;
private final RelationalConverter converter;
FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper, FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper,
JdbcAggregateOperations entityOperations) { JdbcAggregateOperations entityOperations, RelationalConverter converter, ProjectionFactory projectionFactory) {
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations); this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations, converter,
projectionFactory);
} }
FetchableFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<R> resultType, 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,
RelationalConverter converter,
ProjectionFactory projectionFactory) {
super(example, sort, limit, resultType, fieldsToInclude); super(example, sort, limit, resultType, fieldsToInclude, projectionFactory, converter);
this.exampleMapper = exampleMapper; this.exampleMapper = exampleMapper;
this.entityOperations = entityOperations; this.entityOperations = entityOperations;
this.converter = converter;
this.projectionFactory = projectionFactory;
} }
@Override @Override
@ -167,6 +176,6 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
List<String> fieldsToInclude) { List<String> fieldsToInclude) {
return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper, return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
this.entityOperations); this.entityOperations, this.converter, this.projectionFactory);
} }
} }

24
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 java.util.function.Function;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.DtoInstantiatingConverter;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort; 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.data.repository.query.FluentQuery;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -41,16 +44,19 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
private final int limit; private final int limit;
private final Class<R> resultType; private final Class<R> resultType;
private final List<String> fieldsToInclude; 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,
ProjectionFactory projectionFactory, RelationalConverter converter) {
FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude) {
this.example = example; this.example = example;
this.sort = sort; this.sort = sort;
this.limit = limit; this.limit = limit;
this.resultType = resultType; this.resultType = resultType;
this.fieldsToInclude = fieldsToInclude; this.fieldsToInclude = fieldsToInclude;
this.projectionFactory = projectionFactory;
this.converter = converter;
} }
@Override @Override
@ -118,10 +124,20 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
return (Function<Object, R>) Function.identity(); return (Function<Object, R>) Function.identity();
} }
EntityProjection<?, ?> entityProjection = converter.introspectProjection(targetType, inputType);
if (entityProjection.isProjection()) {
if (targetType.isInterface()) { if (targetType.isInterface()) {
return o -> projectionFactory.createProjection(targetType, o); 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); return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
} }

6
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.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.relational.repository.query.RelationalExampleMapper; import org.springframework.data.relational.repository.query.RelationalExampleMapper;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.PagingAndSortingRepository;
@ -48,9 +49,11 @@ import org.springframework.util.Assert;
public class SimpleJdbcRepository<T, ID> public class SimpleJdbcRepository<T, ID>
implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
private final JdbcAggregateOperations entityOperations; private final JdbcAggregateOperations entityOperations;
private final PersistentEntity<T, ?> entity; private final PersistentEntity<T, ?> entity;
private final RelationalExampleMapper exampleMapper; private final RelationalExampleMapper exampleMapper;
private final JdbcConverter converter;
public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity<T, ?> entity, public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity<T, ?> entity,
JdbcConverter converter) { JdbcConverter converter) {
@ -60,6 +63,7 @@ public class SimpleJdbcRepository<T, ID>
this.entityOperations = entityOperations; this.entityOperations = entityOperations;
this.entity = entity; this.entity = entity;
this.converter = converter;
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext()); this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
} }
@ -197,7 +201,7 @@ public class SimpleJdbcRepository<T, ID>
Assert.notNull(queryFunction, "Query function must not be null"); Assert.notNull(queryFunction, "Query function must not be null");
FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example, FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
example.getProbeType(), this.exampleMapper, this.entityOperations); example.getProbeType(), this.exampleMapper, this.entityOperations, this.converter, this.projectionFactory);
return queryFunction.apply(fluentQuery); return queryFunction.apply(fluentQuery);
} }

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

@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
@ -51,7 +52,16 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.domain.*; import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.query.Query;
@ -64,8 +74,8 @@ import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.Sequence; import org.springframework.data.relational.core.mapping.Sequence;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.LockMode;
@ -1187,6 +1197,32 @@ public class JdbcRepositoryIntegrationTests {
assertThat(matches).isEqualTo(2); 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 @Test // GH-1192
void fetchByExampleFluentOnlyInstantFirstSimple() { void fetchByExampleFluentOnlyInstantFirstSimple() {
@ -1888,10 +1924,10 @@ public class JdbcRepositoryIntegrationTests {
static class DummyEntity { static class DummyEntity {
@Id Long idProp;
String name; String name;
Instant pointInTime; Instant pointInTime;
OffsetDateTime offsetDateTime; OffsetDateTime offsetDateTime;
@Id private Long idProp;
boolean flag; boolean flag;
AggregateReference<DummyEntity, Long> ref; AggregateReference<DummyEntity, Long> ref;
Direction direction; Direction direction;

Loading…
Cancel
Save