Skip to content

Commit df86b88

Browse files
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
1 parent 233cbcf commit df86b88

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -972,8 +972,8 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
972972

973973
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
974974
List<R> results = new ArrayList<>();
975+
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
975976
for (R instance : instances) {
976-
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
977977
T domainObject = converter.convert(instance);
978978

979979
T savedEntity = saveImpl(domainObject, pps, stateMachine);

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,9 @@ <T, R> Flux<R> doSave(Iterable<R> instances, Class<T> domainType) {
382382
projectionFactory, neo4jMappingContext);
383383

384384
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
385+
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
385386
return Flux.fromIterable(instances)
386387
.flatMap(instance -> {
387-
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
388388
T domainObject = converter.convert(instance);
389389

390390
@SuppressWarnings("unchecked")

src/main/java/org/springframework/data/neo4j/core/mapping/EntityFromDtoInstantiatingConverter.java

+29-7
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@
1515
*/
1616
package org.springframework.data.neo4j.core.mapping;
1717

18+
import java.util.Collection;
19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
1822
import org.apiguardian.api.API;
23+
import org.springframework.core.CollectionFactory;
1924
import org.springframework.core.convert.converter.Converter;
2025
import org.springframework.data.mapping.MappingException;
2126
import org.springframework.data.mapping.PersistentEntity;
2227
import org.springframework.data.mapping.PersistentProperty;
2328
import org.springframework.data.mapping.PersistentPropertyAccessor;
2429
import org.springframework.data.mapping.PreferredConstructor;
2530
import org.springframework.data.mapping.PreferredConstructor.Parameter;
26-
import org.springframework.data.mapping.SimplePropertyHandler;
2731
import org.springframework.data.mapping.model.ParameterValueProvider;
2832
import org.springframework.data.util.ClassTypeInformation;
2933
import org.springframework.data.util.ReflectionUtils;
@@ -41,11 +45,13 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
4145
private final Class<?> targetEntityType;
4246
private final Neo4jMappingContext context;
4347

48+
private final Map<Class<?>, EntityFromDtoInstantiatingConverter<?>> converterCache = new ConcurrentHashMap<>();
49+
4450
/**
4551
* Creates a new {@link Converter} to instantiate Entities from DTOs.
4652
*
4753
* @param entityType must not be {@literal null}.
48-
* @param context must not be {@literal null}.
54+
* @param context must not be {@literal null}.
4955
*/
5056
public EntityFromDtoInstantiatingConverter(Class<T> entityType, Neo4jMappingContext context) {
5157

@@ -63,7 +69,8 @@ public T convert(Object dtoInstance) {
6369
return null;
6470
}
6571

66-
PersistentEntity<?, ?> sourceEntity = context.addPersistentEntity(ClassTypeInformation.from(dtoInstance.getClass())).get();
72+
PersistentEntity<?, ?> sourceEntity = context.addPersistentEntity(
73+
ClassTypeInformation.from(dtoInstance.getClass())).get();
6774
PersistentPropertyAccessor<Object> sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance);
6875

6976
PersistentEntity<?, ?> targetEntity = context.getPersistentEntity(targetEntityType);
@@ -78,23 +85,21 @@ public Object getParameterValue(Parameter parameter) {
7885
PersistentProperty<?> targetProperty = targetEntity.getPersistentProperty(parameter.getName());
7986
if (targetProperty == null) {
8087
throw new MappingException("Cannot map constructor parameter " + parameter.getName()
81-
+ " to a property of class " + targetEntityType);
88+
+ " to a property of class " + targetEntityType);
8289
}
8390
return getPropertyValueFor(targetProperty, sourceEntity, sourceAccessor);
8491
}
8592
});
8693

8794
PersistentPropertyAccessor<Object> dtoAccessor = targetEntity.getPropertyAccessor(entity);
88-
targetEntity.doWithProperties((SimplePropertyHandler) property -> {
89-
95+
targetEntity.doWithAll(property -> {
9096
if (constructor.isConstructorParameter(property)) {
9197
return;
9298
}
9399

94100
Object propertyValue = getPropertyValueFor(property, sourceEntity, sourceAccessor);
95101
dtoAccessor.setProperty(property, propertyValue);
96102
});
97-
98103
return entity;
99104
}
100105

@@ -114,6 +119,23 @@ Object getPropertyValueFor(PersistentProperty<?> targetProperty, PersistentEntit
114119
return ReflectionUtils.getPrimitiveDefault(targetPropertyType);
115120
}
116121

122+
if (targetProperty.isAssociation() && targetProperty.isCollectionLike()) {
123+
EntityFromDtoInstantiatingConverter<?> nestedConverter = converterCache.computeIfAbsent(targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, context));
124+
Collection<?> source = (Collection<?>) propertyValue;
125+
if (source == null) {
126+
return CollectionFactory.createCollection(targetPropertyType, 0);
127+
}
128+
Collection<Object> target = CollectionFactory.createApproximateCollection(source, source.size());
129+
source.stream().map(nestedConverter::convert).forEach(target::add);
130+
return target;
131+
}
132+
133+
if (propertyValue != null && !targetPropertyType.isInstance(propertyValue)) {
134+
EntityFromDtoInstantiatingConverter<?> nestedConverter = converterCache.computeIfAbsent(targetProperty.getType(),
135+
t -> new EntityFromDtoInstantiatingConverter<>(t, context));
136+
return nestedConverter.convert(propertyValue);
137+
}
138+
117139
return propertyValue;
118140
}
119141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2474;
17+
18+
import lombok.Data;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.UUID;
23+
24+
/**
25+
* @author Stephen Jackson
26+
*/
27+
@Data
28+
public class CityModelDTO {
29+
private UUID cityId;
30+
private String name;
31+
private String exoticProperty;
32+
33+
public PersonModelDTO mayor;
34+
public List<PersonModelDTO> citizens = new ArrayList<>();
35+
public List<JobRelationshipDTO> cityEmployees = new ArrayList<>();
36+
37+
/**
38+
* Nested projection
39+
*/
40+
@Data
41+
public static class PersonModelDTO {
42+
private UUID personId;
43+
}
44+
45+
/**
46+
* Nested projection
47+
*/
48+
@Data
49+
public static class JobRelationshipDTO {
50+
private PersonModelDTO person;
51+
}
52+
}

src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModelRepository.java

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.neo4j.integration.issues.gh2474;
1717

1818
import java.util.List;
19+
import java.util.Optional;
1920
import java.util.UUID;
2021

2122
import org.springframework.data.domain.Sort;
@@ -28,6 +29,8 @@
2829
*/
2930
public interface CityModelRepository extends Neo4jRepository<CityModel, UUID> {
3031

32+
Optional<CityModelDTO> findByCityId(UUID cityId);
33+
3134
@Query(""
3235
+ "MATCH (n:CityModel)"
3336
+ "RETURN n :#{orderBy(#sort)}")

src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/GH2474IT.java

+44
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919

2020
import java.util.Arrays;
21+
import java.util.Collections;
2122
import java.util.List;
2223

2324
import org.junit.jupiter.api.BeforeEach;
@@ -29,6 +30,7 @@
2930
import org.springframework.data.domain.Sort;
3031
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
3132
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
33+
import org.springframework.data.neo4j.core.Neo4jTemplate;
3234
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
3335
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
3436
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@@ -56,6 +58,12 @@ public class GH2474IT {
5658
@Autowired
5759
CityModelRepository cityModelRepository;
5860

61+
@Autowired
62+
PersonModelRepository personModelRepository;
63+
64+
@Autowired
65+
Neo4jTemplate neo4jTemplate;
66+
5967
@BeforeEach
6068
void setupData() {
6169

@@ -114,6 +122,42 @@ public void testSortOnExoticPropertyCustomQuery() {
114122
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
115123
}
116124

125+
@Test // GH-2475
126+
public void testCityModelProjectionPersistence() {
127+
CityModel cityModel = new CityModel();
128+
cityModel.setName("New Cool City");
129+
cityModel = cityModelRepository.save(cityModel);
130+
131+
PersonModel personModel = new PersonModel();
132+
personModel.setName("Mr. Mayor");
133+
personModel.setAddress("1600 City Avenue");
134+
personModel.setFavoriteFood("tacos");
135+
personModelRepository.save(personModel);
136+
137+
CityModelDTO cityModelDTO = cityModelRepository.findByCityId(cityModel.getCityId())
138+
.orElseThrow(RuntimeException::new);
139+
cityModelDTO.setName("Changed name");
140+
cityModelDTO.setExoticProperty("tigers");
141+
142+
CityModelDTO.PersonModelDTO personModelDTO = new CityModelDTO.PersonModelDTO();
143+
personModelDTO.setPersonId(personModelDTO.getPersonId());
144+
145+
CityModelDTO.JobRelationshipDTO jobRelationshipDTO = new CityModelDTO.JobRelationshipDTO();
146+
jobRelationshipDTO.setPerson(personModelDTO);
147+
148+
cityModelDTO.setMayor(personModelDTO);
149+
cityModelDTO.setCitizens(Collections.singletonList(personModelDTO));
150+
cityModelDTO.setCityEmployees(Collections.singletonList(jobRelationshipDTO));
151+
neo4jTemplate.save(CityModel.class).one(cityModelDTO);
152+
153+
CityModel reloaded = cityModelRepository.findById(cityModel.getCityId())
154+
.orElseThrow(RuntimeException::new);
155+
assertThat(reloaded.getName()).isEqualTo("Changed name");
156+
assertThat(reloaded.getMayor()).isNotNull();
157+
assertThat(reloaded.getCitizens()).hasSize(1);
158+
assertThat(reloaded.getCityEmployees()).hasSize(1);
159+
}
160+
117161
@Configuration
118162
@EnableTransactionManagement
119163
@EnableNeo4jRepositories
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2474;
17+
18+
import org.springframework.data.neo4j.repository.Neo4jRepository;
19+
20+
import java.util.UUID;
21+
22+
/**
23+
* @author Stephen Jackson
24+
*/
25+
public interface PersonModelRepository extends Neo4jRepository<PersonModel, UUID> {
26+
}

0 commit comments

Comments
 (0)