Read properties for DTO projections only once.

We now skip property population for properties that are populated through an entity creator (constructor/factory method).

Closes #1725
This commit is contained in:
Mark Paluch
2024-01-29 09:00:17 +01:00
parent be94f7695c
commit 52fe74e3cf
2 changed files with 56 additions and 16 deletions

View File

@@ -29,6 +29,7 @@ import io.r2dbc.spi.test.MockRowMetadata;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
@@ -43,6 +44,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.data.mapping.context.PersistentEntities;
@@ -91,7 +93,7 @@ public class R2dbcEntityTemplateUnitTests {
.bindMarkers(PostgresDialect.INSTANCE.getBindMarkersFactory()).build();
R2dbcCustomConversions conversions = R2dbcCustomConversions.of(PostgresDialect.INSTANCE, new MoneyConverter(),
new RowConverter(), new RowDocumentConverter());
new RowConverter(), new RowDocumentConverter(), new PkConverter());
entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE,
new MappingR2dbcConverter(new R2dbcMappingContext(), conversions));
@@ -632,6 +634,53 @@ public class R2dbcEntityTemplateUnitTests {
}).verifyComplete();
}
@Test // GH-1725
void projectDtoShouldReadPropertiesOnce() {
MockRowMetadata metadata = MockRowMetadata.builder()
.columnMetadata(MockColumnMetadata.builder().name("number").type(R2dbcType.BINARY).build()).build();
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
byteBuffer.putDouble(1.2);
byteBuffer.flip();
MockResult result = MockResult.builder()
.row(MockRow.builder().identified("number", Object.class, byteBuffer).metadata(metadata).build()).build();
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
entityTemplate.select(WithDoubleHolder.class).as(DoubleHolderProjection.class).all().as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual.number.number).isCloseTo(1.2d, withinPercentage(1d));
}).verifyComplete();
}
@ReadingConverter
static class PkConverter implements Converter<ByteBuffer, DoubleHolder> {
@Nullable
@Override
public DoubleHolder convert(ByteBuffer source) {
return new DoubleHolder(source.getDouble());
}
}
static class WithDoubleHolder {
DoubleHolder number;
}
static class DoubleHolderProjection {
DoubleHolder number;
public DoubleHolderProjection(DoubleHolder number) {
this.number = number;
}
}
record DoubleHolder(double number) {
}
@Test // GH-1696
void shouldConsiderRowDocumentConverter() {

View File

@@ -238,17 +238,8 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl
EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(mappedEntity);
R instance = instantiator.createInstance(mappedEntity, provider);
PersistentPropertyAccessor<R> accessor = mappedEntity.getPropertyAccessor(instance);
populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);
PersistentPropertyAccessor<?> convertingAccessor = new ConvertingPropertyAccessor<>(accessor,
getConversionService());
RelationalPropertyValueProvider valueProvider = newValueProvider(documentAccessor, evaluator, context);
readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue());
return accessor.getBean();
return populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);
}
private Object doReadOrProject(ConversionContext context, RowDocument source, TypeInformation<?> typeHint,
@@ -452,11 +443,7 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl
EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(entity);
S instance = instantiator.createInstance(entity, provider);
if (entity.requiresPropertyPopulation()) {
return populateProperties(context, entity, documentAccessor, evaluator, instance);
}
return instance;
return populateProperties(context, entity, documentAccessor, evaluator, instance);
}
@Override
@@ -509,6 +496,10 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl
private <S> S populateProperties(ConversionContext context, RelationalPersistentEntity<S> entity,
RowDocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) {
if (!entity.requiresPropertyPopulation()) {
return instance;
}
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
getConversionService());