Browse Source

#475 - Refine projection result mapping.

We now differentiate the result mapping based on projection type and query type. Previously, all projections used the domain type to map results first and then serve as backend for the projection proxy/DTO creation.
We now use direct result to DTO mapping for String-based queries to allow for a greater flexibility when declaring DTO result types. Interface-based projections and derived queries remain using the two-step process of result to domain type mapping and then mapping the domain type into the projection.
pull/1188/head
Mark Paluch 5 years ago
parent
commit
b8f4e8b765
No known key found for this signature in database
GPG Key ID: 51A00FA751B91849
  1. 15
      src/main/asciidoc/reference/r2dbc-repositories.adoc
  2. 16
      src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java
  3. 7
      src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
  4. 33
      src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java
  5. 45
      src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java
  6. 28
      src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java

15
src/main/asciidoc/reference/r2dbc-repositories.adoc

@ -354,6 +354,21 @@ template.update(other).subscribe(); // emits OptimisticLockingFailureException @@ -354,6 +354,21 @@ template.update(other).subscribe(); // emits OptimisticLockingFailureException
:projection-collection: Flux
include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2]
[[projections.resultmapping]]
==== Result Mapping
A query method returning an Interface- or DTO projection is backed by results produced by the actual query.
Interface projections generally rely on mapping results onto the domain type first to consider potential `@Column` type mappings and the actual projection proxy uses a potentially partially materialized entity to expose projection data.
Result mapping for DTO projections depends on the actual query type.
Derived queries use the domain type to map results, and Spring Data creates DTO instances solely from properties available on the domain type.
Declaring properties in your DTO that are not available on the domain type is not supported.
String-based queries use a different approach since the actual query, specifically the field projection, and result type declaration are close together.
DTO projections used with query methods annotated with `@Query` map query results directly into the DTO type.
Field mappings on the domain type are not considered.
Using the DTO type directly, your query method can benefit from a more dynamic projection that isn't restricted to the domain model.
include::../{spring-data-commons-docs}/entity-callbacks.adoc[leveloffset=+1]
include::./r2dbc-entity-callbacks.adoc[leveloffset=+2]

16
src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java

@ -103,10 +103,10 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { @@ -103,10 +103,10 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery {
fetchSpec = (FetchSpec) boundQuery.map(row -> true);
} else if (requiresMapping()) {
Class<?> resultType = resolveResultType(processor);
EntityRowMapper rowMapper = new EntityRowMapper<>(resultType, converter);
Class<?> typeToRead = resolveResultType(processor);
EntityRowMapper rowMapper = new EntityRowMapper<>(typeToRead, converter);
if (converter.isSimpleType(resultType)) {
if (converter.isSimpleType(typeToRead)) {
fetchSpec = new UnwrapOptionalFetchSpecAdapter<>(
boundQuery.map((row, rowMetadata) -> Optional.ofNullable(rowMapper.apply(row, rowMetadata))));
@ -125,17 +125,17 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { @@ -125,17 +125,17 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery {
return execution.execute(fetchSpec, processor.getReturnedType().getDomainType(), tableName);
}
private boolean requiresMapping() {
return !isModifyingQuery();
}
private Class<?> resolveResultType(ResultProcessor resultProcessor) {
Class<?> resolveResultType(ResultProcessor resultProcessor) {
ReturnedType returnedType = resultProcessor.getReturnedType();
return returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType();
}
private boolean requiresMapping() {
return !isModifyingQuery();
}
private R2dbcQueryExecution getExecutionToWrap(ReturnedType returnedType) {
if (isModifyingQuery()) {

7
src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java

@ -26,6 +26,7 @@ import org.springframework.data.r2dbc.repository.Query; @@ -26,6 +26,7 @@ import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -156,6 +157,12 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { @@ -156,6 +157,12 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
});
}
@Override
Class<?> resolveResultType(ResultProcessor resultProcessor) {
Class<?> returnedType = resultProcessor.getReturnedType().getReturnedType();
return !returnedType.isInterface() ? returnedType : super.resolveResultType(resultProcessor);
}
private Mono<R2dbcSpELExpressionEvaluator> getSpelEvaluator(RelationalParameterAccessor accessor) {

33
src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java

@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.Value;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -142,8 +143,8 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg @@ -142,8 +143,8 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
}).verifyComplete();
}
@Test
void shouldFindApplyingProjection() {
@Test // gh-475
void shouldFindApplyingInterfaceProjection() {
shouldInsertNewItems();
@ -156,6 +157,20 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg @@ -156,6 +157,20 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
}).verifyComplete();
}
@Test // gh-475
void shouldByStringQueryApplyingDtoProjection() {
shouldInsertNewItems();
repository.findAsDtoProjection() //
.map(LegoDto::getName) //
.collectList() //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
}
@Test // gh-344
void shouldFindApplyingDistinctProjection() {
@ -355,6 +370,9 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg @@ -355,6 +370,9 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
Flux<Named> findAsProjection();
@Query("SELECT name from legoset")
Flux<LegoDto> findAsDtoProjection();
Flux<Named> findDistinctBy();
Mono<LegoSet> findByManual(int manual);
@ -400,6 +418,17 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg @@ -400,6 +418,17 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
@Id Integer id;
}
@Value
static class LegoDto {
String name;
String unknown;
public LegoDto(String name, String unknown) {
this.name = name;
this.unknown = unknown;
}
}
interface Named {
String getName();
}

45
src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java

@ -37,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @@ -37,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Id;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
@ -621,6 +622,32 @@ class PartTreeR2dbcQueryUnitTests { @@ -621,6 +622,32 @@ class PartTreeR2dbcQueryUnitTests {
+ ".foo FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1");
}
@Test // gh-475
void createsQueryToFindByOpenProjection() throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findOpenProjectionBy");
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
dataAccessStrategy);
BindableQuery bindableQuery = createQuery(queryMethod, r2dbcQuery);
assertThat(bindableQuery.get()).isEqualTo(
"SELECT users.id, users.first_name, users.last_name, users.date_of_birth, users.age, users.active FROM "
+ TABLE);
}
@Test // gh-475
void createsDtoProjectionQuery() throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findAsDtoProjectionBy");
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
dataAccessStrategy);
BindableQuery bindableQuery = createQuery(queryMethod, r2dbcQuery);
assertThat(bindableQuery.get()).isEqualTo(
"SELECT users.id, users.first_name, users.last_name, users.date_of_birth, users.age, users.active FROM "
+ TABLE);
}
@Test // gh-363
void createsQueryForCountProjection() throws Exception {
@ -721,6 +748,10 @@ class PartTreeR2dbcQueryUnitTests { @@ -721,6 +748,10 @@ class PartTreeR2dbcQueryUnitTests {
Mono<UserProjection> findDistinctByFirstName(String firstName);
Mono<OpenUserProjection> findOpenProjectionBy();
Mono<UserDtoProjection> findAsDtoProjectionBy();
Mono<Integer> deleteByFirstName(String firstName);
Mono<Long> countByFirstName(String firstName);
@ -744,4 +775,18 @@ class PartTreeR2dbcQueryUnitTests { @@ -744,4 +775,18 @@ class PartTreeR2dbcQueryUnitTests {
String getFoo();
}
interface OpenUserProjection {
String getFirstName();
@Value("#firstName")
String getFoo();
}
static class UserDtoProjection {
String firstName;
String unknown;
}
}

28
src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java

@ -258,6 +258,22 @@ public class StringBasedR2dbcQueryUnitTests { @@ -258,6 +258,22 @@ public class StringBasedR2dbcQueryUnitTests {
verifyNoMoreInteractions(bindSpec);
}
@Test // gh-475
void usesDomainTypeForInterfaceProjectionResultMapping() {
StringBasedR2dbcQuery query = getQueryMethod("findAsInterfaceProjection");
assertThat(query.resolveResultType(query.getQueryMethod().getResultProcessor())).isEqualTo(Person.class);
}
@Test // gh-475
void usesDtoTypeForDtoResultMapping() {
StringBasedR2dbcQuery query = getQueryMethod("findAsDtoProjection");
assertThat(query.resolveResultType(query.getQueryMethod().getResultProcessor())).isEqualTo(PersonDto.class);
}
private StringBasedR2dbcQuery getQueryMethod(String name, Class<?>... args) {
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
@ -306,8 +322,20 @@ public class StringBasedR2dbcQueryUnitTests { @@ -306,8 +322,20 @@ public class StringBasedR2dbcQueryUnitTests {
@Query("SELECT * FROM person WHERE lastname = :name")
Person queryWithEnum(MyEnum myEnum);
@Query("SELECT * FROM person")
PersonDto findAsDtoProjection();
@Query("SELECT * FROM person")
PersonProjection findAsInterfaceProjection();
}
static class PersonDto {
}
interface PersonProjection {}
static class Person {
String name;

Loading…
Cancel
Save