Skip to content

Commit a62d92b

Browse files
committed
DATAGRAPH-1412 - Deeper relationship mapping for custom queries.
1 parent 469a4b7 commit a62d92b

File tree

2 files changed

+70
-41
lines changed

2 files changed

+70
-41
lines changed

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

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
118118
if (queryRoot == null) {
119119
throw new MappingException(String.format("Could not find mappable nodes or relationships inside %s for %s", mapAccessor, rootNodeDescription));
120120
} else {
121-
return map(queryRoot, rootNodeDescription, new KnownObjects(), new HashSet<>());
121+
return map(queryRoot, queryRoot, rootNodeDescription, new KnownObjects(), new HashSet<>());
122122
}
123123
} catch (Exception e) {
124124
throw new MappingException("Error mapping " + mapAccessor.toString(), e);
@@ -195,33 +195,23 @@ private static MapAccessor mergeRootNodeWithRecord(Node node, MapAccessor record
195195
}
196196

197197
/**
198-
* @param queryResult The original query result
198+
* @param queryResult The original query result or a reduced form like a node or similar
199+
* @param allValues The original query result
199200
* @param nodeDescription The node description of the current entity to be mapped from the result
200201
* @param knownObjects The current list of known objects
202+
* @param processedSegments Path segments already processed in the mapping process. Only applies to path-based queries
201203
* @param <ET> As in entity type
202-
* @return
204+
* @return The mapped entity
203205
*/
204-
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects, Set<Path.Segment> processedSegments) {
205-
return map(queryResult, nodeDescription, knownObjects, null, processedSegments);
206+
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects, Set<Path.Segment> processedSegments) {
207+
return map(queryResult, allValues, nodeDescription, knownObjects, null, processedSegments);
206208
}
207209

208-
/**
209-
* @param queryResult The original query result
210-
* @param nodeDescription The node description of the current entity to be mapped from the result
211-
* @param knownObjects The current list of known objects
212-
* @param lastMappedEntity Previous created entity for relationships, can be null.
213-
* @param <ET> As in entity type
214-
* @return
215-
*/
216-
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects,
210+
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects,
217211
@Nullable Object lastMappedEntity, Set<Path.Segment> processedSegments) {
218212

219213
// if the given result does not contain an identifier to the mapped object cannot get temporarily saved
220-
Long internalId = queryResult.get(Constants.NAME_OF_INTERNAL_ID).isNull()
221-
? null
222-
: queryResult instanceof Node
223-
? ((Node) queryResult).id()
224-
: queryResult.get(Constants.NAME_OF_INTERNAL_ID).asLong();
214+
Long internalId = getInternalId(queryResult);
225215

226216
Supplier<Object> mappedObjectSupplier = () -> {
227217

@@ -233,7 +223,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
233223

234224
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
235225

236-
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
226+
ET instance = instantiate(concreteNodeDescription, queryResult, allValues, knownObjects, relationships,
237227
nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, processedSegments);
238228

239229
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
@@ -253,7 +243,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
253243
knownObjects.storeObject(internalId, instance);
254244
// Fill associations
255245
concreteNodeDescription.doWithAssociations(
256-
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects, processedSegments));
246+
populateFrom(queryResult, allValues, propertyAccessor, isConstructorParameter, relationships, knownObjects, processedSegments));
257247
}
258248
ET bean = propertyAccessor.getBean();
259249

@@ -270,6 +260,15 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
270260
return (ET) mappedObject;
271261
}
272262

263+
@Nullable
264+
private Long getInternalId(@NonNull MapAccessor queryResult) {
265+
return queryResult instanceof Node
266+
? (Long) ((Node) queryResult).id()
267+
: queryResult.get(Constants.NAME_OF_INTERNAL_ID) == null || queryResult.get(Constants.NAME_OF_INTERNAL_ID).isNull()
268+
? null
269+
: queryResult.get(Constants.NAME_OF_INTERNAL_ID).asLong();
270+
}
271+
273272
/**
274273
* Returns the list of labels for the entity to be created from the "main" node returned.
275274
*
@@ -289,7 +288,7 @@ private List<String> getLabels(MapAccessor queryResult) {
289288
return labels;
290289
}
291290

292-
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, KnownObjects knownObjects,
291+
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, MapAccessor allValues, KnownObjects knownObjects,
293292
Collection<RelationshipDescription> relationships, Collection<String> surplusLabels, Object lastMappedEntity,
294293
Set<Path.Segment> processedSegments) {
295294

@@ -300,7 +299,7 @@ public Object getParameterValue(PreferredConstructor.Parameter parameter) {
300299
Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(parameter.getName());
301300

302301
if (matchingProperty.isRelationship()) {
303-
return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships, processedSegments).orElse(null);
302+
return createInstanceOfRelationships(matchingProperty, values, allValues, knownObjects, relationships, processedSegments).orElse(null);
304303
} else if (matchingProperty.isDynamicLabels()) {
305304
return createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels);
306305
} else if (matchingProperty.isEntityInRelationshipWithProperties()) {
@@ -335,7 +334,7 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
335334
};
336335
}
337336

338-
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
337+
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, MapAccessor allValues,
339338
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
340339
Collection<RelationshipDescription> relationshipDescriptions, KnownObjects knownObjects, Set<Path.Segment> processedSegments) {
341340
return association -> {
@@ -345,13 +344,13 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
345344
return;
346345
}
347346

348-
createInstanceOfRelationships(persistentProperty, queryResult, knownObjects, relationshipDescriptions, processedSegments)
347+
createInstanceOfRelationships(persistentProperty, queryResult, allValues, knownObjects, relationshipDescriptions, processedSegments)
349348
.ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value));
350349
};
351350
}
352351

353352
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values,
354-
KnownObjects knownObjects, Collection<RelationshipDescription> relationshipDescriptions, Set<Path.Segment> processedSegments) {
353+
MapAccessor allValues, KnownObjects knownObjects, Collection<RelationshipDescription> relationshipDescriptions, Set<Path.Segment> processedSegments) {
355354

356355
RelationshipDescription relationshipDescription = relationshipDescriptions.stream()
357356
.filter(r -> r.getFieldName().equals(persistentProperty.getName())).findFirst().get();
@@ -392,11 +391,11 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
392391
mappedObjectHandler = (type, mappedObject) -> value.add(mappedObject);
393392
}
394393

395-
Value list = values.get(relationshipDescription.generateRelatedNodesCollectionName());
394+
Value list = allValues.get(relationshipDescription.generateRelatedNodesCollectionName());
396395

397396
List<Object> relationshipsAndProperties = new ArrayList<>();
398397

399-
boolean isGeneratedPathBased = values.containsKey(Constants.NAME_OF_PATHS);
398+
boolean isGeneratedPathBased = allValues.containsKey(Constants.NAME_OF_PATHS);
400399

401400
Predicate<Value> isList = entry -> typeSystem.LIST().isTypeOf(entry);
402401

@@ -426,11 +425,11 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
426425
continue;
427426
}
428427
processedSegments.add(segment);
429-
Object mappedObject = map(extractNextNodeAndAppendPath(segment.end(), allPaths),
428+
Object mappedObject = map(extractNextNodeAndAppendPath(segment.end(), allPaths), allValues,
430429
concreteTargetNodeDescription, knownObjects, processedSegments);
431430
if (relationshipDescription.hasRelationshipProperties()) {
432431

433-
Object relationshipProperties = map(segment.relationship(),
432+
Object relationshipProperties = map(segment.relationship(), allValues,
434433
(Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(),
435434
knownObjects, mappedObject, processedSegments);
436435
relationshipsAndProperties.add(relationshipProperties);
@@ -449,11 +448,11 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
449448

450449
// find relationships in the result
451450
List<Relationship> allMatchingTypeRelationshipsInResult = StreamSupport
452-
.stream(values.values().spliterator(), false).filter(isList.and(containsOnlyRelationships))
451+
.stream(allValues.values().spliterator(), false).filter(isList.and(containsOnlyRelationships))
453452
.flatMap(entry -> entry.asList(Value::asRelationship).stream()).filter(r -> r.type().equals(relationshipType))
454453
.collect(Collectors.toList());
455454

456-
List<Node> allNodesWithMatchingLabelInResult = StreamSupport.stream(values.values().spliterator(), false)
455+
List<Node> allNodesWithMatchingLabelInResult = StreamSupport.stream(allValues.values().spliterator(), false)
457456
.filter(isList.and(containsOnlyNodes)).flatMap(entry -> entry.asList(Value::asNode).stream())
458457
.filter(n -> n.hasLabel(targetLabel)).collect(Collectors.toList());
459458

@@ -462,16 +461,17 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
462461
}
463462

464463
Function<Relationship, Long> targetIdSelector = relationshipDescription.isIncoming() ? Relationship::startNodeId : Relationship::endNodeId;
465-
464+
Function<Relationship, Long> sourceIdSelector = relationshipDescription.isIncoming() ? Relationship::endNodeId : Relationship::startNodeId;
465+
Long sourceNodeId = getInternalId(values);
466466
for (Node possibleValueNode : allNodesWithMatchingLabelInResult) {
467-
long nodeId = possibleValueNode.id();
467+
long targetNodeId = possibleValueNode.id();
468468

469469
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
470-
if (targetIdSelector.apply(possibleRelationship) == nodeId) {
471-
Object mappedObject = map(possibleValueNode, concreteTargetNodeDescription, knownObjects, processedSegments);
470+
if (targetIdSelector.apply(possibleRelationship) == targetNodeId && sourceIdSelector.apply(possibleRelationship).equals(sourceNodeId)) {
471+
Object mappedObject = map(possibleValueNode, values, concreteTargetNodeDescription, knownObjects, processedSegments);
472472
if (relationshipDescription.hasRelationshipProperties()) {
473473

474-
Object relationshipProperties = map(possibleRelationship,
474+
Object relationshipProperties = map(possibleRelationship, allValues,
475475
(Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(),
476476
knownObjects, mappedObject, processedSegments);
477477
relationshipsAndProperties.add(relationshipProperties);
@@ -487,13 +487,13 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
487487
} else {
488488
for (Value relatedEntity : list.asList(Function.identity())) {
489489

490-
Object valueEntry = map(relatedEntity, concreteTargetNodeDescription, knownObjects, processedSegments);
490+
Object valueEntry = map(relatedEntity, allValues, concreteTargetNodeDescription, knownObjects, processedSegments);
491491

492492
if (relationshipDescription.hasRelationshipProperties()) {
493493
Relationship relatedEntityRelationship = relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP)
494494
.asRelationship();
495495

496-
Object relationshipProperties = map(relatedEntityRelationship,
496+
Object relationshipProperties = map(relatedEntityRelationship, allValues,
497497
(Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(),
498498
knownObjects, valueEntry, processedSegments);
499499
relationshipsAndProperties.add(relationshipProperties);

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,31 @@ void findSliceShouldWork(@Autowired PersonRepository repository) {
563563
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
564564
assertThat(slice.hasNext()).isFalse();
565565
}
566+
567+
@Test // DATAGRAPH-1412
568+
void customFindMapsDeepRelationships(@Autowired PetRepository repository) {
569+
long petNode1Id;
570+
long petNode2Id;
571+
long petNode3Id;
572+
573+
try (Session session = createSession()) {
574+
Record record = session.run("CREATE " + "(p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), "
575+
+ "(p2)-[:Has]->(p3:Pet{name: 'Pet3'}) " + "RETURN p1, p2, p3").single();
576+
577+
petNode1Id = record.get("p1").asNode().id();
578+
petNode2Id = record.get("p2").asNode().id();
579+
petNode3Id = record.get("p3").asNode().id();
580+
}
581+
582+
Pet loadedPet = repository.customQueryWithDeepRelationshipMapping(petNode1Id);
583+
584+
Pet comparisonPet2 = new Pet(petNode2Id, "Pet2");
585+
Pet comparisonPet3 = new Pet(petNode3Id, "Pet3");
586+
assertThat(loadedPet.getFriends()).containsExactlyInAnyOrder(comparisonPet2);
587+
588+
Pet pet2 = loadedPet.getFriends().get(loadedPet.getFriends().indexOf(comparisonPet2));
589+
assertThat(pet2.getFriends()).containsExactly(comparisonPet3);
590+
}
566591
}
567592

568593
@Nested
@@ -645,7 +670,6 @@ void findDeepSameLabelsAndTypeRelationships(@Autowired PetRepository repository)
645670
}
646671

647672
Pet loadedPet = repository.findById(petNode1Id).get();
648-
649673
Pet comparisonPet2 = new Pet(petNode2Id, "Pet2");
650674
Pet comparisonPet3 = new Pet(petNode3Id, "Pet3");
651675
assertThat(loadedPet.getFriends()).containsExactlyInAnyOrder(comparisonPet2);
@@ -3147,7 +3171,12 @@ interface PersonWithRelationshipWithPropertiesRepository
31473171
PersonWithRelationshipWithProperties findByHobbiesHobbyName(String hobbyName);
31483172
}
31493173

3150-
interface PetRepository extends Neo4jRepository<Pet, Long> {}
3174+
interface PetRepository extends Neo4jRepository<Pet, Long> {
3175+
3176+
@Query("MATCH (p:Pet)-[r1:Has]->(p2:Pet)-[r2:Has]->(p3:Pet) " +
3177+
"where id(p) = $petNode1Id return p, collect(r1), collect(p2), collect(r2), collect(p3)")
3178+
Pet customQueryWithDeepRelationshipMapping(@Param("petNode1Id") long petNode1Id);
3179+
}
31513180

31523181
interface RelationshipRepository extends Neo4jRepository<PersonWithRelationship, Long> {
31533182

0 commit comments

Comments
 (0)