Skip to content

Commit 6db0178

Browse files
committed
GH-2345 - Add missing relationships during mapping.
If it is possible (the structure is mutable), SDN should try to add relationships to objects if those information come in subsequent records. closes #2345
1 parent 0021f2b commit 6db0178

File tree

3 files changed

+209
-22
lines changed

3 files changed

+209
-22
lines changed

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

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
101101
@Override
102102
public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
103103

104+
knownObjects.nextRecord();
104105
@SuppressWarnings("unchecked") // ¯\_(ツ)_/¯
105106
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity<R>) nodeDescriptionStore.getNodeDescription(targetType);
106107
MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription);
@@ -274,30 +275,14 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
274275
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
275276
.getNodeDescription();
276277

277-
boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType());
278278
ET instance = instantiate(concreteNodeDescription, queryResult,
279279
nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, relationshipsFromResult, nodesFromResult);
280280

281281
knownObjects.removeFromInCreation(internalId);
282-
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
283282

284-
if (concreteNodeDescription.requiresPropertyPopulation()) {
285-
286-
// Fill simple properties
287-
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
288-
.getPersistenceConstructor()::isConstructorParameter;
289-
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
290-
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType);
291-
concreteNodeDescription.doWithProperties(handler);
292-
293-
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
294-
// reference the start again. Because it is getting still constructed, it won't be in the knownObjects
295-
// store unless we temporarily put it there.
296-
knownObjects.storeObject(internalId, instance);
297-
// Fill associations
298-
concreteNodeDescription.doWithAssociations(
299-
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationshipsFromResult, nodesFromResult));
300-
}
283+
populateProperties(queryResult, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false);
284+
285+
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
301286
ET bean = propertyAccessor.getBean();
302287

303288
// save final state of the bean
@@ -310,10 +295,58 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
310295
if (mappedObject == null) {
311296
mappedObject = mappedObjectSupplier.get();
312297
knownObjects.storeObject(internalId, mappedObject);
298+
} else if (knownObjects.alreadyMappedInPreviousRecord(internalId)) {
299+
// If the object were created in a run before, it _could_ have missing relationships
300+
// (e.g. due to incomplete fetching by a custom query)
301+
// in such cases we will add the additional data from the next record.
302+
// This can and should only work for
303+
// 1. mutable owning types
304+
// AND (!!!)
305+
// 2. mutable target types
306+
// because we cannot just create new instances
307+
populateProperties(queryResult, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true);
313308
}
314309
return mappedObject;
315310
}
316311

312+
313+
private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, Long internalId,
314+
ET mappedObject, @Nullable Object lastMappedEntity,
315+
Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult, boolean objectAlreadyMapped) {
316+
317+
List<String> allLabels = getLabels(queryResult, nodeDescription);
318+
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
319+
.deriveConcreteNodeDescription(nodeDescription, allLabels);
320+
321+
@SuppressWarnings("unchecked")
322+
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
323+
.getNodeDescription();
324+
325+
if (!concreteNodeDescription.requiresPropertyPopulation()) {
326+
return;
327+
}
328+
329+
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(mappedObject);
330+
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
331+
.getPersistenceConstructor()::isConstructorParameter;
332+
333+
// if the object were mapped before, we assume that at least all properties are populated
334+
if (!objectAlreadyMapped) {
335+
boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType());
336+
// Fill simple properties
337+
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
338+
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType);
339+
concreteNodeDescription.doWithProperties(handler);
340+
}
341+
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
342+
// reference the start again. Because it is getting still constructed, it won't be in the knownObjects
343+
// store unless we temporarily put it there.
344+
knownObjects.storeObject(internalId, mappedObject);
345+
// Fill associations
346+
concreteNodeDescription.doWithAssociations(
347+
populateFrom(queryResult, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult));
348+
}
349+
317350
@Nullable
318351
private Long getInternalId(@NonNull MapAccessor queryResult) {
319352
return queryResult instanceof Node
@@ -405,8 +438,9 @@ public <T> T getParameterValue(PreferredConstructor.Parameter<T, Neo4jPersistent
405438
}
406439

407440
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
408-
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
409-
Collection<String> surplusLabels, Object targetNode, boolean ownerIsKotlinType) {
441+
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
442+
Collection<String> surplusLabels, @Nullable Object targetNode, boolean ownerIsKotlinType) {
443+
410444
return property -> {
411445
if (isConstructorParameter.test(property)) {
412446
return;
@@ -428,20 +462,55 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
428462
};
429463
}
430464

465+
@Nullable
431466
private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawType, @Nullable Object value) {
432467

433468
return value == null && !ownerIsKotlinType && rawType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(rawType) : value;
434469
}
435470

436471
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
437-
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter, Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
472+
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
473+
boolean objectAlreadyMapped, Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
474+
438475
return association -> {
439476

440477
Neo4jPersistentProperty persistentProperty = association.getInverse();
478+
441479
if (isConstructorParameter.test(persistentProperty)) {
442480
return;
443481
}
444482

483+
if (objectAlreadyMapped) {
484+
485+
// avoid multiple instances of the "same" object
486+
boolean willCreateNewInstance = persistentProperty.getWither() != null;
487+
if (willCreateNewInstance) {
488+
throw new MappingException("Cannot create a new instance of an already existing object.");
489+
}
490+
491+
Object propertyValue = propertyAccessor.getProperty(persistentProperty);
492+
493+
boolean propertyValueNotNull = propertyValue != null;
494+
495+
boolean populatedCollection = persistentProperty.isCollectionLike()
496+
&& propertyValueNotNull
497+
&& !((Collection<?>) propertyValue).isEmpty();
498+
499+
boolean populatedMap = persistentProperty.isMap()
500+
&& propertyValueNotNull
501+
&& !((Map<?, ?>) propertyValue).isEmpty();
502+
503+
boolean populatedScalarValue = !persistentProperty.isCollectionLike()
504+
&& propertyValueNotNull;
505+
506+
boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue;
507+
508+
// avoid unnecessary re-assignment of values
509+
if (propertyAlreadyPopulated) {
510+
return;
511+
}
512+
}
513+
445514
createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, relationshipsFromResult, nodesFromResult)
446515
.ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value));
447516
};
@@ -662,6 +731,7 @@ static class KnownObjects {
662731
private final Lock write = lock.writeLock();
663732

664733
private final Map<Long, Object> internalIdStore = new HashMap<>();
734+
private final Map<Long, Boolean> internalNextRecord = new HashMap<>();
665735
private final Set<Long> idsInCreation = new HashSet<>();
666736

667737
private void storeObject(@Nullable Long internalId, Object object) {
@@ -672,6 +742,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
672742
write.lock();
673743
idsInCreation.remove(internalId);
674744
internalIdStore.put(internalId, object);
745+
internalNextRecord.put(internalId, false);
675746
} finally {
676747
write.unlock();
677748
}
@@ -733,5 +804,32 @@ private void removeFromInCreation(@Nullable Long internalId) {
733804
write.unlock();
734805
}
735806
}
807+
808+
private boolean alreadyMappedInPreviousRecord(@Nullable Long internalId) {
809+
if (internalId == null) {
810+
return false;
811+
}
812+
try {
813+
814+
read.lock();
815+
816+
Boolean nextRecord = internalNextRecord.get(internalId);
817+
818+
if (nextRecord != null) {
819+
return nextRecord;
820+
}
821+
822+
} finally {
823+
read.unlock();
824+
}
825+
return false;
826+
}
827+
828+
/**
829+
* Mark all currently existing objects as mapped.
830+
*/
831+
private void nextRecord() {
832+
internalNextRecord.replaceAll((x, y) -> true);
833+
}
736834
}
737835
}

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
import org.springframework.data.neo4j.integration.shared.common.FriendshipRelationship;
117117
import org.springframework.data.neo4j.integration.shared.common.Hobby;
118118
import org.springframework.data.neo4j.integration.shared.common.ImmutablePerson;
119+
import org.springframework.data.neo4j.integration.shared.common.ImmutablePet;
119120
import org.springframework.data.neo4j.integration.shared.common.Inheritance;
120121
import org.springframework.data.neo4j.integration.shared.common.KotlinPerson;
121122
import org.springframework.data.neo4j.integration.shared.common.LikesHobbyRelationship;
@@ -709,6 +710,27 @@ void customFindMapsDeepRelationships(@Autowired PetRepository repository) {
709710
assertThat(pet2.getFriends()).containsExactly(comparisonPet3);
710711
}
711712

713+
@Test // GH-2345
714+
void customFindHydratesIncompleteCustomQueryObjectsCorrect(@Autowired PetRepository repository) {
715+
doWithSession(session ->
716+
session.run("CREATE (:Pet{name: 'Luna'})-[:Has]->(:Pet{name:'Luna'})-[:Has]->(:Pet{name:'Daphne'})").consume()
717+
);
718+
719+
List<Pet> pets = repository.findLunas();
720+
assertThat(pets).hasSize(2);
721+
722+
assertThat(pets).allMatch(pet -> !pet.getFriends().isEmpty());
723+
}
724+
725+
@Test // GH-2345
726+
void customFindFailsOnHydrationOfCustomQueryObjectsIfImmutable(@Autowired ImmutablePetRepository repository) {
727+
doWithSession(session ->
728+
session.run("CREATE (:ImmutablePet{name: 'Luna'})-[:Has]->(:ImmutablePet{name:'Luna'})-[:Has]->(:ImmutablePet{name:'Daphne'})").consume()
729+
);
730+
731+
assertThatExceptionOfType(MappingException.class).isThrownBy(repository::findLunas);
732+
}
733+
712734
@Test // DATAGRAPH-1409
713735
void findPageWithCustomQuery(@Autowired PetRepository repository) {
714736

@@ -4128,6 +4150,16 @@ interface PetRepository extends Neo4jRepository<Pet, Long> {
41284150
long countByFriendsNameAndFriendsFriendsName(String friendName, String friendFriendName);
41294151

41304152
boolean existsByName(String name);
4153+
4154+
@Query("MATCH (n:Pet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:Pet) return n, collect(r), collect(m)")
4155+
List<Pet> findLunas();
4156+
}
4157+
4158+
interface ImmutablePetRepository extends Neo4jRepository<ImmutablePet, Long> {
4159+
4160+
@Query("MATCH (n:ImmutablePet) where n.name='Luna' OPTIONAL MATCH (n)-[r:Has]->(m:ImmutablePet) return n, collect(r), collect(m)")
4161+
List<ImmutablePet> findLunas();
4162+
41314163
}
41324164

41334165
interface OneToOneRepository extends Neo4jRepository<OneToOneSource, String> {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2011-2021 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.shared.common;
17+
18+
import org.springframework.data.annotation.PersistenceConstructor;
19+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
20+
import org.springframework.data.neo4j.core.schema.Id;
21+
import org.springframework.data.neo4j.core.schema.Node;
22+
import org.springframework.data.neo4j.core.schema.Relationship;
23+
24+
import java.util.Set;
25+
26+
/**
27+
* @author Gerrit Meier
28+
*/
29+
@Node
30+
public class ImmutablePet {
31+
32+
@Id
33+
@GeneratedValue
34+
public final Long id;
35+
36+
public final String name;
37+
38+
@Relationship("Has")
39+
public final Set<ImmutablePet> friends;
40+
41+
@PersistenceConstructor
42+
public ImmutablePet(Long id, String name) {
43+
this.id = id;
44+
this.name = name;
45+
this.friends = null;
46+
}
47+
48+
public ImmutablePet(Long id, String name, Set<ImmutablePet> friends) {
49+
this.id = id;
50+
this.name = name;
51+
this.friends = friends;
52+
}
53+
54+
public ImmutablePet withFriends(Set<ImmutablePet> newFriends) {
55+
return new ImmutablePet(id, name, newFriends);
56+
}
57+
}

0 commit comments

Comments
 (0)