Skip to content

Commit c1cb92f

Browse files
committed
DATAGRAPH-1433 - Improve path-based query generation.
1 parent f21e7c9 commit c1cb92f

File tree

3 files changed

+161
-31
lines changed

3 files changed

+161
-31
lines changed

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

Lines changed: 116 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
import org.neo4j.cypherdsl.core.Node;
4343
import org.neo4j.cypherdsl.core.Parameter;
4444
import org.neo4j.cypherdsl.core.Relationship;
45+
import org.neo4j.cypherdsl.core.RelationshipPattern;
4546
import org.neo4j.cypherdsl.core.Statement;
4647
import org.neo4j.cypherdsl.core.StatementBuilder;
4748
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
4849
import org.neo4j.cypherdsl.core.SymbolicName;
4950
import org.springframework.data.mapping.MappingException;
5051
import org.springframework.data.mapping.PersistentProperty;
52+
import org.springframework.data.neo4j.core.schema.Relationship.Direction;
5153
import org.springframework.lang.NonNull;
5254
import org.springframework.lang.Nullable;
5355
import org.springframework.util.Assert;
@@ -329,10 +331,8 @@ public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescripti
329331

330332
SymbolicName nodeName = Constants.NAME_OF_ROOT_NODE;
331333
List<RelationshipDescription> processedRelationships = new ArrayList<>();
332-
boolean containsPossibleCircles = containsPossibleCircles(nodeDescription);
333334

334-
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeField,
335-
processedRelationships, containsPossibleCircles);
335+
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeField, processedRelationships);
336336
}
337337

338338
// recursive entry point for relationships in return statement
@@ -341,19 +341,17 @@ private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> no
341341

342342
Predicate<String> includeAllFields = (field) -> true;
343343
// Because we are getting called recursive, there cannot be any circle
344-
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeAllFields, processedRelationships,
345-
false);
344+
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeAllFields, processedRelationships);
346345
}
347346

348347
private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeDescription, SymbolicName nodeName,
349-
Predicate<String> includeProperty, List<RelationshipDescription> processedRelationships,
350-
boolean containsPossibleCircles) {
348+
Predicate<String> includeProperty, List<RelationshipDescription> processedRelationships) {
351349

352350
List<Object> propertiesProjection = projectNodeProperties(nodeDescription, nodeName, includeProperty);
353351
List<Object> contentOfProjection = new ArrayList<>(propertiesProjection);
354-
if (containsPossibleCircles) {
355-
Relationship pattern = anyNode(nodeName).relationshipBetween(anyNode(),
356-
collectAllRelationshipTypes(nodeDescription)).unbounded();
352+
if (nodeDescription.containsPossibleCircles()) {
353+
Node node = anyNode(nodeName);
354+
RelationshipPattern pattern = createRelationships(node, nodeDescription.getRelationships());
357355
NamedPath p = Cypher.path("p").definedBy(pattern);
358356
contentOfProjection.add(Constants.NAME_OF_PATHS);
359357
contentOfProjection.add(Cypher.listBasedOn(p).returning(p));
@@ -364,41 +362,116 @@ private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeD
364362
return Cypher.anyNode(nodeName).project(contentOfProjection);
365363
}
366364

367-
private boolean containsPossibleCircles(NodeDescription<?> nodeDescription) {
368-
Collection<RelationshipDescription> relationships = nodeDescription.getRelationships();
365+
private RelationshipPattern createRelationships(Node node, Collection<RelationshipDescription> relationshipDescriptions) {
366+
RelationshipPattern relationship;
369367

370-
Set<RelationshipDescription> processedRelationships = new HashSet<>();
371-
for (RelationshipDescription relationship : relationships) {
372-
if (processedRelationships.contains(relationship)) {
373-
return true;
368+
Direction determinedDirection = determineDirection(relationshipDescriptions);
369+
if (Direction.OUTGOING.equals(determinedDirection)) {
370+
relationship = node.relationshipTo(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
371+
} else if (Direction.INCOMING.equals(determinedDirection)) {
372+
relationship = node.relationshipFrom(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
373+
} else {
374+
relationship = node.relationshipBetween(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
375+
}
376+
377+
Set<RelationshipDescription> processedRelationshipDescriptions = new HashSet<>(relationshipDescriptions);
378+
for (RelationshipDescription relationshipDescription : relationshipDescriptions) {
379+
Collection<RelationshipDescription> relationships = relationshipDescription.getTarget().getRelationships();
380+
if (relationships.size() > 0) {
381+
relationship = createRelationships(relationship, relationships, processedRelationshipDescriptions)
382+
.relationship;
383+
}
384+
}
385+
386+
return relationship;
387+
}
388+
389+
private RelationshipProcessState createRelationships(RelationshipPattern existingRelationship,
390+
Collection<RelationshipDescription> relationshipDescriptions,
391+
Set<RelationshipDescription> processedRelationshipDescriptions) {
392+
393+
RelationshipPattern relationship = existingRelationship;
394+
String[] relationshipTypes = collectAllRelationshipTypes(relationshipDescriptions);
395+
if (processedRelationshipDescriptions.containsAll(relationshipDescriptions)) {
396+
return new RelationshipProcessState(
397+
relationship.relationshipBetween(anyNode(),
398+
relationshipTypes).unbounded().min(0), true);
399+
}
400+
processedRelationshipDescriptions.addAll(relationshipDescriptions);
401+
402+
// we can process through the path
403+
if (relationshipDescriptions.size() == 1) {
404+
RelationshipDescription relationshipDescription = relationshipDescriptions.iterator().next();
405+
switch (relationshipDescription.getDirection()) {
406+
case OUTGOING:
407+
relationship = existingRelationship.relationshipTo(anyNode(),
408+
collectFirstLevelRelationshipTypes(relationshipDescriptions)).unbounded().min(0).max(1);
409+
break;
410+
case INCOMING:
411+
relationship = existingRelationship.relationshipFrom(anyNode(),
412+
collectFirstLevelRelationshipTypes(relationshipDescriptions)).unbounded().min(0).max(1);
413+
break;
414+
default:
415+
relationship = existingRelationship.relationshipBetween(anyNode(),
416+
collectFirstLevelRelationshipTypes(relationshipDescriptions)).unbounded().min(0).max(1);
374417
}
375-
processedRelationships.add(relationship);
376-
if (containsPossibleCircles(relationship.getTarget(), processedRelationships)) {
377-
return true;
418+
419+
RelationshipProcessState relationships = createRelationships(relationship,
420+
relationshipDescription.getTarget().getRelationships(), processedRelationshipDescriptions);
421+
422+
if (!relationships.done) {
423+
relationship = relationships.relationship;
378424
}
425+
} else {
426+
Direction determinedDirection = determineDirection(relationshipDescriptions);
427+
if (Direction.OUTGOING.equals(determinedDirection)) {
428+
relationship = existingRelationship.relationshipTo(anyNode(), relationshipTypes).unbounded().min(0);
429+
} else if (Direction.INCOMING.equals(determinedDirection)) {
430+
relationship = existingRelationship.relationshipFrom(anyNode(), relationshipTypes).unbounded().min(0);
431+
} else {
432+
relationship = existingRelationship.relationshipBetween(anyNode(), relationshipTypes).unbounded().min(0);
433+
}
434+
return new RelationshipProcessState(relationship, true);
379435
}
380-
return false;
436+
return new RelationshipProcessState(relationship, false);
381437
}
382438

383-
private boolean containsPossibleCircles(NodeDescription<?> nodeDescription, Set<RelationshipDescription> processedRelationships) {
384-
Collection<RelationshipDescription> relationships = nodeDescription.getRelationships();
439+
@Nullable
440+
Direction determineDirection(Collection<RelationshipDescription> relationshipDescriptions) {
385441

386-
for (RelationshipDescription relationship : relationships) {
387-
if (processedRelationships.contains(relationship)) {
388-
return true;
442+
Direction direction = null;
443+
for (RelationshipDescription relationshipDescription : relationshipDescriptions) {
444+
if (direction == null) {
445+
direction = relationshipDescription.getDirection();
389446
}
390-
processedRelationships.add(relationship);
391-
if (containsPossibleCircles(relationship.getTarget(), processedRelationships)) {
392-
return true;
447+
if (!direction.equals(relationshipDescription.getDirection())) {
448+
return null;
393449
}
394450
}
395-
return false;
451+
return direction;
396452
}
397453

398-
private String[] collectAllRelationshipTypes(NodeDescription<?> nodeDescription) {
454+
private String[] collectFirstLevelRelationshipTypes(Collection<RelationshipDescription> relationshipDescriptions) {
399455
Set<String> relationshipTypes = new HashSet<>();
400456

401-
for (RelationshipDescription relationshipDescription : nodeDescription.getRelationships()) {
457+
for (RelationshipDescription relationshipDescription : relationshipDescriptions) {
458+
String relationshipType = relationshipDescription.getType();
459+
if (relationshipTypes.contains(relationshipType)) {
460+
continue;
461+
}
462+
if (relationshipDescription.isDynamic()) {
463+
relationshipTypes.clear();
464+
continue;
465+
}
466+
relationshipTypes.add(relationshipType);
467+
}
468+
return relationshipTypes.toArray(new String[0]);
469+
}
470+
471+
private String[] collectAllRelationshipTypes(Collection<RelationshipDescription> relationshipDescriptions) {
472+
Set<String> relationshipTypes = new HashSet<>();
473+
474+
for (RelationshipDescription relationshipDescription : relationshipDescriptions) {
402475
String relationshipType = relationshipDescription.getType();
403476
if (relationshipTypes.contains(relationshipType)) {
404477
continue;
@@ -421,6 +494,7 @@ private void collectAllRelationshipTypes(NodeDescription<?> nodeDescription, Set
421494
continue;
422495
}
423496
processedRelationshipTypes.add(relationshipType);
497+
collectAllRelationshipTypes(relationshipDescription.getTarget(), processedRelationshipTypes);
424498
}
425499
}
426500

@@ -546,4 +620,15 @@ private void addMapProjection(String name, Object projection, List<Object> proje
546620
private static Condition conditionOrNoCondition(@Nullable Condition condition) {
547621
return condition == null ? Conditions.noCondition() : condition;
548622
}
623+
624+
private static class RelationshipProcessState {
625+
private final RelationshipPattern relationship;
626+
private final boolean done;
627+
628+
RelationshipProcessState(RelationshipPattern relationship, boolean done) {
629+
this.relationship = relationship;
630+
this.done = done;
631+
}
632+
}
633+
549634
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
8787

8888
private final Lazy<Boolean> isRelationshipPropertiesEntity;
8989

90+
private final Lazy<Boolean> containsPossibleCircles;
91+
9092
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
9193
super(information);
9294

@@ -97,6 +99,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
9799
this.dynamicLabelsProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast)
98100
.filter(Neo4jPersistentProperty::isDynamicLabels).findFirst().orElse(null));
99101
this.isRelationshipPropertiesEntity = Lazy.of(() -> isAnnotationPresent(RelationshipProperties.class));
102+
this.containsPossibleCircles = Lazy.of(this::calculatePossibleCircles);
100103
}
101104

102105
/*
@@ -410,4 +413,40 @@ public void setParentNodeDescription(NodeDescription<?> parent) {
410413
private NodeDescription<?> getParentNodeDescription() {
411414
return parentNodeDescription;
412415
}
416+
417+
@Override
418+
public boolean containsPossibleCircles() {
419+
return containsPossibleCircles.get();
420+
}
421+
422+
private boolean calculatePossibleCircles() {
423+
Collection<RelationshipDescription> relationships = getRelationships();
424+
425+
Set<RelationshipDescription> processedRelationships = new HashSet<>();
426+
for (RelationshipDescription relationship : relationships) {
427+
if (processedRelationships.contains(relationship)) {
428+
return true;
429+
}
430+
processedRelationships.add(relationship);
431+
if (calculatePossibleCircles(relationship.getTarget(), processedRelationships)) {
432+
return true;
433+
}
434+
}
435+
return false;
436+
}
437+
438+
private boolean calculatePossibleCircles(NodeDescription<?> nodeDescription, Set<RelationshipDescription> processedRelationships) {
439+
Collection<RelationshipDescription> relationships = nodeDescription.getRelationships();
440+
441+
for (RelationshipDescription relationship : relationships) {
442+
if (processedRelationships.contains(relationship)) {
443+
return true;
444+
}
445+
processedRelationships.add(relationship);
446+
if (calculatePossibleCircles(relationship.getTarget(), processedRelationships)) {
447+
return true;
448+
}
449+
}
450+
return false;
451+
}
413452
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,10 @@ default Expression getIdExpression() {
128128

129129
return this.getIdDescription().asIdExpression();
130130
}
131+
132+
/**
133+
* @return Information if the domain would contain schema circles.
134+
*/
135+
boolean containsPossibleCircles();
136+
131137
}

0 commit comments

Comments
 (0)