GH-2475 - Recursively hydrate entities from DTOs.

This is necessary so that all properties are actually filled before the entity is passed to the persister.

Fixes #2475
This commit is contained in:
Michael Simons
2022-02-07 19:14:34 +01:00
parent 58d0978814
commit 9614633195
7 changed files with 156 additions and 9 deletions

View File

@@ -916,8 +916,8 @@ public final class Neo4jTemplate implements
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
List<R> results = new ArrayList<>();
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
for (R instance : instances) {
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
T domainObject = converter.convert(instance);
T savedEntity = saveImpl(domainObject, pps, stateMachine);

View File

@@ -351,9 +351,9 @@ public final class ReactiveNeo4jTemplate implements
projectionFactory, neo4jMappingContext);
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
return Flux.fromIterable(instances)
.flatMap(instance -> {
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
T domainObject = converter.convert(instance);
return saveImpl(domainObject, pps, stateMachine)

View File

@@ -15,7 +15,12 @@
*/
package org.springframework.data.neo4j.core.mapping;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apiguardian.api.API;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -23,7 +28,6 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.ReflectionUtils;
@@ -41,11 +45,13 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
private final Class<?> targetEntityType;
private final Neo4jMappingContext context;
private final Map<Class<?>, EntityFromDtoInstantiatingConverter<?>> converterCache = new ConcurrentHashMap<>();
/**
* Creates a new {@link Converter} to instantiate Entities from DTOs.
*
* @param entityType must not be {@literal null}.
* @param context must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public EntityFromDtoInstantiatingConverter(Class<T> entityType, Neo4jMappingContext context) {
@@ -63,7 +69,8 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
return null;
}
PersistentEntity<?, ?> sourceEntity = context.addPersistentEntity(ClassTypeInformation.from(dtoInstance.getClass())).get();
PersistentEntity<?, ?> sourceEntity = context.addPersistentEntity(
ClassTypeInformation.from(dtoInstance.getClass())).get();
PersistentPropertyAccessor<Object> sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance);
PersistentEntity<?, ?> targetEntity = context.getPersistentEntity(targetEntityType);
@@ -78,15 +85,14 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
PersistentProperty<?> targetProperty = targetEntity.getPersistentProperty(parameter.getName());
if (targetProperty == null) {
throw new MappingException("Cannot map constructor parameter " + parameter.getName()
+ " to a property of class " + targetEntityType);
+ " to a property of class " + targetEntityType);
}
return getPropertyValueFor(targetProperty, sourceEntity, sourceAccessor);
}
});
PersistentPropertyAccessor<Object> dtoAccessor = targetEntity.getPropertyAccessor(entity);
targetEntity.doWithProperties((SimplePropertyHandler) property -> {
targetEntity.doWithAll(property -> {
if (constructor.isConstructorParameter(property)) {
return;
}
@@ -94,7 +100,6 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
Object propertyValue = getPropertyValueFor(property, sourceEntity, sourceAccessor);
dtoAccessor.setProperty(property, propertyValue);
});
return entity;
}
@@ -114,6 +119,23 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
return ReflectionUtils.getPrimitiveDefault(targetPropertyType);
}
if (targetProperty.isAssociation() && targetProperty.isCollectionLike()) {
EntityFromDtoInstantiatingConverter<?> nestedConverter = converterCache.computeIfAbsent(targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, context));
Collection<?> source = (Collection<?>) propertyValue;
if (source == null) {
return CollectionFactory.createCollection(targetPropertyType, 0);
}
Collection<Object> target = CollectionFactory.createApproximateCollection(source, source.size());
source.stream().map(nestedConverter::convert).forEach(target::add);
return target;
}
if (propertyValue != null && !targetPropertyType.isInstance(propertyValue)) {
EntityFromDtoInstantiatingConverter<?> nestedConverter = converterCache.computeIfAbsent(targetProperty.getType(),
t -> new EntityFromDtoInstantiatingConverter<>(t, context));
return nestedConverter.convert(propertyValue);
}
return propertyValue;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.issues.gh2474;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @author Stephen Jackson
*/
@Data
public class CityModelDTO {
private UUID cityId;
private String name;
private String exoticProperty;
public PersonModelDTO mayor;
public List<PersonModelDTO> citizens = new ArrayList<>();
public List<JobRelationshipDTO> cityEmployees = new ArrayList<>();
/**
* Nested projection
*/
@Data
public static class PersonModelDTO {
private UUID personId;
}
/**
* Nested projection
*/
@Data
public static class JobRelationshipDTO {
private PersonModelDTO person;
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.integration.issues.gh2474;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Sort;
@@ -28,6 +29,8 @@ import org.springframework.data.neo4j.repository.query.Query;
*/
public interface CityModelRepository extends Neo4jRepository<CityModel, UUID> {
Optional<CityModelDTO> findByCityId(UUID cityId);
@Query(""
+ "MATCH (n:CityModel)"
+ "RETURN n :#{orderBy(#sort)}")

View File

@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.issues.gh2474;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
@@ -29,6 +30,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@@ -56,6 +58,12 @@ public class GH2474IT {
@Autowired
CityModelRepository cityModelRepository;
@Autowired
PersonModelRepository personModelRepository;
@Autowired
Neo4jTemplate neo4jTemplate;
@BeforeEach
void setupData() {
@@ -114,6 +122,42 @@ public class GH2474IT {
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
}
@Test // GH-2475
public void testCityModelProjectionPersistence() {
CityModel cityModel = new CityModel();
cityModel.setName("New Cool City");
cityModel = cityModelRepository.save(cityModel);
PersonModel personModel = new PersonModel();
personModel.setName("Mr. Mayor");
personModel.setAddress("1600 City Avenue");
personModel.setFavoriteFood("tacos");
personModelRepository.save(personModel);
CityModelDTO cityModelDTO = cityModelRepository.findByCityId(cityModel.getCityId())
.orElseThrow(RuntimeException::new);
cityModelDTO.setName("Changed name");
cityModelDTO.setExoticProperty("tigers");
CityModelDTO.PersonModelDTO personModelDTO = new CityModelDTO.PersonModelDTO();
personModelDTO.setPersonId(personModelDTO.getPersonId());
CityModelDTO.JobRelationshipDTO jobRelationshipDTO = new CityModelDTO.JobRelationshipDTO();
jobRelationshipDTO.setPerson(personModelDTO);
cityModelDTO.setMayor(personModelDTO);
cityModelDTO.setCitizens(Collections.singletonList(personModelDTO));
cityModelDTO.setCityEmployees(Collections.singletonList(jobRelationshipDTO));
neo4jTemplate.save(CityModel.class).one(cityModelDTO);
CityModel reloaded = cityModelRepository.findById(cityModel.getCityId())
.orElseThrow(RuntimeException::new);
assertThat(reloaded.getName()).isEqualTo("Changed name");
assertThat(reloaded.getMayor()).isNotNull();
assertThat(reloaded.getCitizens()).hasSize(1);
assertThat(reloaded.getCityEmployees()).hasSize(1);
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.issues.gh2474;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import java.util.UUID;
/**
* @author Stephen Jackson
*/
public interface PersonModelRepository extends Neo4jRepository<PersonModel, UUID> {
}