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:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
Reference in New Issue
Block a user