diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 86b372442..99c18c308 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -916,8 +916,8 @@ public final class Neo4jTemplate implements NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); List results = new ArrayList<>(); + EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); for (R instance : instances) { - EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); T domainObject = converter.convert(instance); T savedEntity = saveImpl(domainObject, pps, stateMachine); diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 372831956..1a894b22f 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -351,9 +351,9 @@ public final class ReactiveNeo4jTemplate implements projectionFactory, neo4jMappingContext); NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext); + EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); return Flux.fromIterable(instances) .flatMap(instance -> { - EntityFromDtoInstantiatingConverter converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext); T domainObject = converter.convert(instance); return saveImpl(domainObject, pps, stateMachine) diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java index 423e1f500..32a02f4da 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java @@ -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 implements Converter targetEntityType; private final Neo4jMappingContext context; + private final Map, 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 entityType, Neo4jMappingContext context) { @@ -63,7 +69,8 @@ public final class EntityFromDtoInstantiatingConverter implements Converter sourceEntity = context.addPersistentEntity(ClassTypeInformation.from(dtoInstance.getClass())).get(); + PersistentEntity sourceEntity = context.addPersistentEntity( + ClassTypeInformation.from(dtoInstance.getClass())).get(); PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance); PersistentEntity targetEntity = context.getPersistentEntity(targetEntityType); @@ -78,15 +85,14 @@ public final class EntityFromDtoInstantiatingConverter implements Converter 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 dtoAccessor = targetEntity.getPropertyAccessor(entity); - targetEntity.doWithProperties((SimplePropertyHandler) property -> { - + targetEntity.doWithAll(property -> { if (constructor.isConstructorParameter(property)) { return; } @@ -94,7 +100,6 @@ public final class EntityFromDtoInstantiatingConverter implements Converter implements Converter nestedConverter = converterCache.computeIfAbsent(targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, context)); + Collection source = (Collection) propertyValue; + if (source == null) { + return CollectionFactory.createCollection(targetPropertyType, 0); + } + Collection 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; } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java new file mode 100644 index 000000000..f55026f0d --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelDTO.java @@ -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 citizens = new ArrayList<>(); + public List cityEmployees = new ArrayList<>(); + + /** + * Nested projection + */ + @Data + public static class PersonModelDTO { + private UUID personId; + } + + /** + * Nested projection + */ + @Data + public static class JobRelationshipDTO { + private PersonModelDTO person; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java index b5e03c543..f70b0ecc9 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java @@ -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 { + Optional findByCityId(UUID cityId); + @Query("" + "MATCH (n:CityModel)" + "RETURN n :#{orderBy(#sort)}") diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java index 347b04833..a84871224 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java @@ -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 diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java new file mode 100644 index 000000000..b6e891e71 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/PersonModelRepository.java @@ -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 { +}