diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcDtoInstantiatingConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcDtoInstantiatingConverter.java new file mode 100644 index 0000000000..984a94800b --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcDtoInstantiatingConverter.java @@ -0,0 +1,106 @@ +package org.springframework.data.jdbc.core.convert; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.data.mapping.*; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Spring {@link Converter} to create instances of the given DTO type from the source value handed into the conversion + * while also handling Aggregate References + * + * @author Mark Paluch + * @author Oliver Drotbohm + * @author Paul Jones + * @since 3.3 + */ +public class JdbcDtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Create a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param instantiators must not be {@literal null}. + */ + public JdbcDtoInstantiatingConverter(Class dtoType, + MappingContext, ? extends PersistentProperty> context, + EntityInstantiators instantiators) { + + Assert.notNull(dtoType, "DTO type must not be null"); + Assert.notNull(context, "MappingContext must not be null"); + Assert.notNull(instantiators, "EntityInstantiators must not be null"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = instantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + @NonNull + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + PersistentEntity> sourceEntity = context + .getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + PersistentEntity> targetEntity = context.getRequiredPersistentEntity(targetType); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + @Nullable + public Object getParameterValue(Parameter parameter) { + + String name = parameter.getName(); + + if (name == null) { + throw new IllegalArgumentException(String.format("Parameter %s does not have a name", parameter)); + } + + return sourceAccessor.getProperty(sourceEntity.getRequiredPersistentProperty(name)); + } + }); + + PersistentPropertyAccessor targetAccessor = targetEntity.getPropertyAccessor(dto); + InstanceCreatorMetadata> creator = targetEntity.getInstanceCreatorMetadata(); + + targetEntity.doWithProperties((SimplePropertyHandler) property -> { + + if ((creator != null) && creator.isCreatorParameter(property)) { + return; + } + + targetAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getRequiredPersistentProperty(property.getName()))); + }); + + targetEntity.doWithAssociations((SimpleAssociationHandler) property -> { + if ((creator != null) && creator.isCreatorParameter(property.getInverse())) { + return; + } + + if(property.getInverse().getType().equals(AggregateReference.class)) { + targetAccessor.setProperty(property.getInverse(), + sourceAccessor.getProperty(sourceEntity.getRequiredPersistentProperty(property.getInverse().getName()))); + + } + }); + + return dto; + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java index bebe20a3b0..7cc39ca744 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java @@ -16,7 +16,7 @@ package org.springframework.data.jdbc.repository.query; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.convert.DtoInstantiatingConverter; +import org.springframework.data.jdbc.core.convert.JdbcDtoInstantiatingConverter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -33,6 +33,7 @@ * query and how to process the result in order to get the desired return type. * * @author Mark Paluch + * @author Paul Jones * @since 2.0 */ @FunctionalInterface @@ -52,6 +53,7 @@ interface JdbcQueryExecution { * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. * * @author Mark Paluch + * @author Paul Jones * @since 2.3 */ class ResultProcessingConverter implements Converter { @@ -63,7 +65,7 @@ class ResultProcessingConverter implements Converter { MappingContext, ? extends RelationalPersistentProperty> mappingContext, EntityInstantiators instantiators) { this.processor = processor; - this.converter = Lazy.of(() -> new DtoInstantiatingConverter(processor.getReturnedType().getReturnedType(), + this.converter = Lazy.of(() -> new JdbcDtoInstantiatingConverter(processor.getReturnedType().getReturnedType(), mappingContext, instantiators)); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index e7f475920d..5ac681444e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -104,6 +104,7 @@ * @author Diego Krupitza * @author Christopher Klein * @author Mikhail Polivakha + * @author Paul Jones */ @IntegrationTest public class JdbcRepositoryIntegrationTests { @@ -1286,6 +1287,36 @@ void fetchByExampleFluentOnlyInstantOneValueAsSimple() { assertThat(match.get().getName()).contains(two.getName()); } + @Test + void fetchDtoWithNoArgsConstructorWithAggregateReferencePopulated() { + DummyEntity entity = new DummyEntity(); + entity.setRef(AggregateReference.to(20L)); + entity.setName("Test Dto"); + repository.save(entity); + + assertThat(repository.findById(entity.idProp).orElseThrow().getRef()).isEqualTo(AggregateReference.to(20L)); + + DummyDto foundDto = repository.findDtoByIdProp(entity.idProp).orElseThrow(); + assertThat(foundDto.getName()).isEqualTo("Test Dto"); + assertThat(foundDto.getRef()).isEqualTo(AggregateReference.to(20L)); + + } + + @Test + void fetchDtoWithAllArgsConstructorWithAggregateReferencePopulated() { + DummyEntity entity = new DummyEntity(); + entity.setRef(AggregateReference.to(20L)); + entity.setName("Test Dto"); + repository.save(entity); + + assertThat(repository.findById(entity.idProp).orElseThrow().getRef()).isEqualTo(AggregateReference.to(20L)); + + DummyAllArgsDto foundDto = repository.findAllArgsDtoByIdProp(entity.idProp).orElseThrow(); + assertThat(foundDto.getName()).isEqualTo("Test Dto"); + assertThat(foundDto.getRef()).isEqualTo(AggregateReference.to(20L)); + + } + @Test // GH-1405 void withDelimitedColumnTest() { @@ -1426,6 +1457,9 @@ interface DummyEntityRepository extends CrudRepository, Query @Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION = :direction") List findByEnumType(Direction direction); + + Optional findDtoByIdProp(Long idProp); + Optional findAllArgsDtoByIdProp(Long idProp); } interface RootRepository extends ListCrudRepository { @@ -1834,6 +1868,43 @@ enum Direction { LEFT, CENTER, RIGHT } + static class DummyDto { + @Id Long idProp; + String name; + AggregateReference ref; + + public DummyDto() { + } + + public String getName() { + return name; + } + + public AggregateReference getRef() { + return ref; + } + } + + static class DummyAllArgsDto { + @Id Long idProp; + String name; + AggregateReference ref; + + public DummyAllArgsDto(Long idProp, String name, AggregateReference ref) { + this.idProp = idProp; + this.name = name; + this.ref = ref; + } + + public String getName() { + return name; + } + + public AggregateReference getRef() { + return ref; + } + } + interface DummyProjection { String getName(); }