Skip to content

Commit cc73484

Browse files
committed
GH-2121 - Optimize cyclic schema queries.
Those queries will now return nodes and relationships instead of plain path(s). This format is now the only one supported by SDN for generated queries. Intentionally closes #2121 As a side-effect this commit also closes #2107 closes #2109 closes #2119 Polishing after rebase.
1 parent b5cb295 commit cc73484

File tree

14 files changed

+675
-141
lines changed

14 files changed

+675
-141
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.HashMap;
25+
import java.util.LinkedHashSet;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.Optional;
@@ -614,7 +615,11 @@ public List<T> getResults() {
614615

615616
public Optional<T> getSingleResult() {
616617
try {
617-
return fetchSpec.one();
618+
Optional<T> one = fetchSpec.one();
619+
if (preparedQuery.resultsHaveBeenAggregated()) {
620+
return one.map(aggregatedResults -> (T) ((LinkedHashSet<?>) aggregatedResults).iterator().next());
621+
}
622+
return one;
618623
} catch (NoSuchRecordException e) {
619624
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
620625
// So there has been an incorrect result size, but not to few results but to many.
@@ -623,7 +628,11 @@ public Optional<T> getSingleResult() {
623628
}
624629

625630
public T getRequiredSingleResult() {
626-
return fetchSpec.one().orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
631+
Optional<T> one = fetchSpec.one();
632+
if (preparedQuery.resultsHaveBeenAggregated()) {
633+
one = one.map(aggregatedResults -> (T) ((LinkedHashSet<?>) aggregatedResults).iterator().next());
634+
}
635+
return one.orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
627636
}
628637
}
629638
}

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,30 +164,37 @@ private static class AggregatingMappingFunction implements BiFunction<TypeSystem
164164
private Collection<?> aggregateList(TypeSystem t, Value value) {
165165

166166
if (MappingSupport.isListContainingOnly(t.LIST(), t.PATH()).test(value)) {
167-
Set<Object> result = new LinkedHashSet<>();
168-
for (Value path : value.values()) {
169-
result.addAll(aggregatePath(t, path, Collections.emptyList()));
170-
}
171-
return result;
167+
return new LinkedHashSet<Object>(aggregatePath(t, value, Collections.emptyList()));
172168
}
173169
return value.asList(v -> target.apply(t, v));
174170
}
175171

176172
private Collection<?> aggregatePath(TypeSystem t, Value value,
177173
List<Map.Entry<String, Value>> additionalValues) {
178-
Path path = value.asPath();
179174

180175
// We are using linked hash sets here so that the order of nodes will be stable and match that of the path.
181176
Set<Object> result = new LinkedHashSet<>();
182177
Set<Value> nodes = new LinkedHashSet<>();
183178
Set<Value> relationships = new LinkedHashSet<>();
184-
Node lastNode = null;
185-
for (Path.Segment segment : path) {
186-
nodes.add(Values.value(segment.start()));
187-
lastNode = segment.end();
188-
relationships.add(Values.value(segment.relationship()));
179+
180+
List<Path> paths = value.hasType(t.PATH())
181+
? Collections.singletonList(value.asPath())
182+
: value.asList(Value::asPath);
183+
184+
for (Path path : paths) {
185+
Node lastNode = null;
186+
for (Path.Segment segment : path) {
187+
Node start = segment.start();
188+
if (start != null) {
189+
nodes.add(Values.value(start));
190+
}
191+
lastNode = segment.end();
192+
relationships.add(Values.value(segment.relationship()));
193+
}
194+
if (lastNode != null) {
195+
nodes.add(Values.value(lastNode));
196+
}
189197
}
190-
nodes.add(Values.value(lastNode));
191198

192199
// This loop synthesizes a node, it's relationship and all related nodes for all nodes in a path.
193200
// All other nodes must be assumed to somehow related

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Collection;
2828
import java.util.Collections;
2929
import java.util.HashMap;
30+
import java.util.LinkedHashSet;
3031
import java.util.List;
3132
import java.util.Map;
3233
import java.util.function.Function;
@@ -632,8 +633,8 @@ final class DefaultReactiveExecutableQuery<T> implements ExecutableQuery<T> {
632633
public Flux<T> getResults() {
633634

634635
return fetchSpec.all().switchOnFirst((signal, f) -> {
635-
if (preparedQuery.resultsHaveBeenAggregated()) {
636-
return f.flatMap(nested -> Flux.fromIterable((Collection<T>) nested).distinct());
636+
if (signal.hasValue() && preparedQuery.resultsHaveBeenAggregated()) {
637+
return f.flatMap(nested -> Flux.fromIterable((Collection<T>) nested).distinct()).distinct();
637638
}
638639
return f;
639640
});
@@ -645,7 +646,12 @@ public Flux<T> getResults() {
645646
*/
646647
public Mono<T> getSingleResult() {
647648
try {
648-
return fetchSpec.one();
649+
return fetchSpec.one().map(t -> {
650+
if (t instanceof LinkedHashSet) {
651+
return (T) ((LinkedHashSet<?>) t).iterator().next();
652+
}
653+
return t;
654+
});
649655
} catch (NoSuchRecordException e) {
650656
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
651657
// So there has been an incorrect result size, but not to few results but to many.

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

Lines changed: 138 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,22 @@
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
2727
import java.util.Collection;
28+
import java.util.Collections;
2829
import java.util.HashSet;
2930
import java.util.List;
3031
import java.util.Set;
3132
import java.util.function.Predicate;
3233
import java.util.function.UnaryOperator;
34+
import java.util.stream.Collectors;
3335

3436
import org.apiguardian.api.API;
3537
import org.neo4j.cypherdsl.core.Condition;
3638
import org.neo4j.cypherdsl.core.Conditions;
3739
import org.neo4j.cypherdsl.core.Cypher;
3840
import org.neo4j.cypherdsl.core.Expression;
41+
import org.neo4j.cypherdsl.core.FunctionInvocation;
3942
import org.neo4j.cypherdsl.core.Functions;
43+
import org.neo4j.cypherdsl.core.ListComprehension;
4044
import org.neo4j.cypherdsl.core.MapProjection;
4145
import org.neo4j.cypherdsl.core.NamedPath;
4246
import org.neo4j.cypherdsl.core.Node;
@@ -100,7 +104,13 @@ public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescri
100104
* @return An ongoing match
101105
*/
102106
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription,
103-
@Nullable Condition condition) {
107+
@Nullable Condition condition) {
108+
109+
return prepareMatchOf(nodeDescription, condition, Collections.emptyList());
110+
}
111+
112+
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription,
113+
@Nullable Condition condition, List<String> includedProperties) {
104114

105115
String primaryLabel = nodeDescription.getPrimaryLabel();
106116
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
@@ -111,7 +121,99 @@ public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescri
111121
expressions.add(Constants.NAME_OF_ROOT_NODE);
112122
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
113123

114-
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(new Expression[] {}));
124+
if (nodeDescription.containsPossibleCircles(includedProperties)) {
125+
return createPathMatchWithCondition(nodeDescription, includedProperties, condition, rootNode);
126+
} else {
127+
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(new Expression[] {}));
128+
}
129+
}
130+
131+
private StatementBuilder.OrderableOngoingReadingAndWithWithoutWhere createPathMatchWithCondition(
132+
NodeDescription<?> nodeDescription, List<String> includedProperties, @Nullable Condition condition, Node rootNode) {
133+
134+
return createPathMatchWithCondition(null, nodeDescription, includedProperties, condition, rootNode);
135+
}
136+
137+
public StatementBuilder.OrderableOngoingReadingAndWithWithoutWhere createPathMatchWithCondition(
138+
@Nullable StatementBuilder.OngoingReadingWithoutWhere previousMatches,
139+
NodeDescription<?> nodeDescription, List<String> includedProperties, @Nullable Condition condition, Node rootNode) {
140+
141+
List<Expression> expressions1 = new ArrayList<>();
142+
List<Expression> expressions2 = new ArrayList<>();
143+
144+
String aliasedPathName = "pathPattern";
145+
Predicate<String> includeField = s -> includedProperties.isEmpty() || includedProperties.contains(s);
146+
Collection<RelationshipDescription> relationships = getRelationshipDescriptionsUpAndDown(nodeDescription, includeField);
147+
RelationshipPattern patternPath = createRelationships(rootNode, relationships);
148+
NamedPath path = Cypher.path("p").definedBy(patternPath);
149+
150+
// nested nodes flatMap: reduce(...reduce(...))
151+
SymbolicName outerNodesAccumulator = Cypher.name("a");
152+
SymbolicName outerNodesVariable = Cypher.name("b");
153+
SymbolicName innerNodesAccumulator = Cypher.name("c");
154+
SymbolicName innerNodesVariable = Cypher.name("d");
155+
SymbolicName innerNodesListIterator = Cypher.name("e");
156+
ListComprehension innerNodesListComprehension = Cypher.listWith(innerNodesListIterator)
157+
.in(Cypher.name(aliasedPathName)).returning(Functions.nodes(innerNodesListIterator));
158+
159+
FunctionInvocation innerNodesReduce = createInnerReduce(innerNodesAccumulator, innerNodesVariable,
160+
innerNodesListComprehension);
161+
162+
FunctionInvocation outerNodesReduce = createOuterReduce(outerNodesAccumulator, outerNodesVariable,
163+
innerNodesReduce);
164+
165+
// nested relationships flatMap: reduce(...reduce(...))
166+
SymbolicName outerRelationshipsAccumulator = Cypher.name("f");
167+
SymbolicName outerRelationshipsVariable = Cypher.name("g");
168+
SymbolicName innerRelationshipsAccumulator = Cypher.name("h");
169+
SymbolicName innerRelationshipsVariable = Cypher.name("i");
170+
SymbolicName innerRelationshipsListIterator = Cypher.name("j");
171+
ListComprehension innerRelationshipsListComprehension = Cypher.listWith(innerRelationshipsListIterator)
172+
.in(Cypher.name(aliasedPathName)).returning(Functions.relationships(innerRelationshipsListIterator));
173+
174+
FunctionInvocation innerRelationshipReduce = createInnerReduce(innerRelationshipsAccumulator,
175+
innerRelationshipsVariable, innerRelationshipsListComprehension);
176+
177+
FunctionInvocation outerRelationshipsReduce = createOuterReduce(outerRelationshipsAccumulator,
178+
outerRelationshipsVariable, innerRelationshipReduce);
179+
180+
// WITH n, collect(p) as pathPattern
181+
expressions1.add(Constants.NAME_OF_ROOT_NODE);
182+
expressions1.add(Functions.collect(path).as(aliasedPathName));
183+
// WITH n, reduce(nodes) as __sm__, reduce(relationships) as __sr__
184+
expressions2.add(Constants.NAME_OF_ROOT_NODE);
185+
expressions2.add(outerNodesReduce.as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
186+
expressions2.add(outerRelationshipsReduce.as(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
187+
188+
StatementBuilder.OngoingReadingWithoutWhere match = match(path);
189+
190+
if (previousMatches != null) {
191+
match = previousMatches.match(path);
192+
}
193+
194+
return match
195+
.where(conditionOrNoCondition(condition))
196+
.with(expressions1.toArray(new Expression[]{}))
197+
.with(expressions2.toArray(new Expression[]{}));
198+
}
199+
200+
private FunctionInvocation createOuterReduce(SymbolicName outerNodesAccumulator, SymbolicName outerNodesVariable, FunctionInvocation innerNodesReduce) {
201+
return Functions.reduce(outerNodesVariable)
202+
.in(innerNodesReduce)
203+
.map(Cypher.caseExpression()
204+
.when(outerNodesVariable.in(outerNodesAccumulator))
205+
.then(outerNodesAccumulator)
206+
.elseDefault(outerNodesAccumulator.add(outerNodesVariable)))
207+
.accumulateOn(outerNodesAccumulator)
208+
.withInitialValueOf(Cypher.listOf());
209+
}
210+
211+
private FunctionInvocation createInnerReduce(SymbolicName innerNodesAccumulator, SymbolicName innerNodesVariable, ListComprehension innerNodesListComprehension) {
212+
return Functions.reduce(innerNodesVariable)
213+
.in(innerNodesListComprehension)
214+
.map(innerNodesAccumulator.add(innerNodesVariable))
215+
.accumulateOn(innerNodesAccumulator)
216+
.withInitialValueOf(Cypher.listOf());
115217
}
116218

117219
/**
@@ -332,8 +434,8 @@ public Statement prepareDeleteOf(
332434
.build();
333435
}
334436

335-
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription) {
336-
return createReturnStatementForMatch(nodeDescription, null);
437+
public Expression[] createReturnStatementForMatch(NodeDescription<?> nodeDescription) {
438+
return createReturnStatementForMatch(nodeDescription, Collections.emptyList());
337439
}
338440

339441
/**
@@ -372,20 +474,25 @@ public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescripti
372474

373475
/**
374476
* @param nodeDescription Description of the root node
375-
* @param inputProperties A list of Java properties of the domain to be included. Those properties are compared with
477+
* @param includedProperties A list of Java properties of the domain to be included. Those properties are compared with
376478
* the field names of graph properties respectively relationships.
377479
* @return An expresion to be returned by a Cypher statement
378480
*/
379-
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription,
380-
@Nullable List<String> inputProperties) {
481+
public Expression[] createReturnStatementForMatch(NodeDescription<?> nodeDescription,
482+
List<String> includedProperties) {
381483

382-
Predicate<String> includeField = s -> inputProperties == null || inputProperties.isEmpty()
383-
|| inputProperties.contains(s);
384-
385-
SymbolicName nodeName = Constants.NAME_OF_ROOT_NODE;
386484
List<RelationshipDescription> processedRelationships = new ArrayList<>();
387-
388-
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeField, processedRelationships);
485+
if (nodeDescription.containsPossibleCircles(includedProperties)) {
486+
List<Expression> returnExpressions = new ArrayList<>();
487+
Node rootNode = anyNode(Constants.NAME_OF_ROOT_NODE);
488+
returnExpressions.add(rootNode.as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
489+
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
490+
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
491+
return returnExpressions.toArray(new Expression[]{});
492+
} else {
493+
Predicate<String> includeField = s -> includedProperties.isEmpty() || includedProperties.contains(s);
494+
return new Expression[]{projectPropertiesAndRelationships(nodeDescription, Constants.NAME_OF_ROOT_NODE, includeField, processedRelationships)};
495+
}
389496
}
390497

391498
// recursive entry point for relationships in return statement
@@ -398,30 +505,23 @@ private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> no
398505
}
399506

400507
private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeDescription, SymbolicName nodeName,
401-
Predicate<String> includeProperty, List<RelationshipDescription> processedRelationships) {
508+
Predicate<String> includedProperties, List<RelationshipDescription> processedRelationships) {
402509

403-
List<Object> propertiesProjection = projectNodeProperties(nodeDescription, nodeName, includeProperty);
510+
List<Object> propertiesProjection = projectNodeProperties(nodeDescription, nodeName, includedProperties);
404511
List<Object> contentOfProjection = new ArrayList<>(propertiesProjection);
405512

406-
Collection<RelationshipDescription> relationships = getRelationshipDescriptionsUpAndDown(nodeDescription);
407-
relationships.removeIf(r -> !includeProperty.test(r.getFieldName()));
513+
Collection<RelationshipDescription> relationships = getRelationshipDescriptionsUpAndDown(nodeDescription, includedProperties);
514+
relationships.removeIf(r -> !includedProperties.test(r.getFieldName()));
408515

409-
if (nodeDescription.containsPossibleCircles()) {
410-
Node node = anyNode(nodeName);
411-
RelationshipPattern pattern = createRelationships(node, relationships);
412-
NamedPath p = Cypher.path("p").definedBy(pattern);
413-
contentOfProjection.add(Constants.NAME_OF_PATHS);
414-
contentOfProjection.add(Cypher.listBasedOn(p).returning(p));
415-
} else {
416-
contentOfProjection.addAll(generateListsFor(relationships, nodeName, processedRelationships));
417-
}
516+
contentOfProjection.addAll(generateListsFor(relationships, nodeName, processedRelationships));
418517
return Cypher.anyNode(nodeName).project(contentOfProjection);
419518
}
420519

421520
@NonNull
422-
static Collection<RelationshipDescription> getRelationshipDescriptionsUpAndDown(NodeDescription<?> nodeDescription) {
423-
Collection<RelationshipDescription> relationships = new HashSet<>(nodeDescription.getRelationships());
521+
static Collection<RelationshipDescription> getRelationshipDescriptionsUpAndDown(NodeDescription<?> nodeDescription,
522+
Predicate<String> includedProperties) {
424523

524+
Collection<RelationshipDescription> relationships = new HashSet<>(nodeDescription.getRelationships());
425525
for (NodeDescription<?> childDescription : nodeDescription.getChildNodeDescriptionsInHierarchy()) {
426526
childDescription.getRelationships().forEach(concreteRelationship -> {
427527

@@ -432,19 +532,25 @@ static Collection<RelationshipDescription> getRelationshipDescriptionsUpAndDown(
432532
}
433533
});
434534
}
435-
return relationships;
535+
536+
return relationships.stream().filter(relationshipDescription ->
537+
includedProperties.test(relationshipDescription.getFieldName()))
538+
.collect(Collectors.toSet());
436539
}
437540

438541
private RelationshipPattern createRelationships(Node node, Collection<RelationshipDescription> relationshipDescriptions) {
439542
RelationshipPattern relationship;
440543

441544
Direction determinedDirection = determineDirection(relationshipDescriptions);
442545
if (Direction.OUTGOING.equals(determinedDirection)) {
443-
relationship = node.relationshipTo(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
546+
relationship = node.relationshipTo(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions))
547+
.min(0).max(1);
444548
} else if (Direction.INCOMING.equals(determinedDirection)) {
445-
relationship = node.relationshipFrom(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
549+
relationship = node.relationshipFrom(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions))
550+
.min(0).max(1);
446551
} else {
447-
relationship = node.relationshipBetween(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions));
552+
relationship = node.relationshipBetween(anyNode(), collectFirstLevelRelationshipTypes(relationshipDescriptions))
553+
.min(0).max(1);
448554
}
449555

450556
Set<RelationshipDescription> processedRelationshipDescriptions = new HashSet<>(relationshipDescriptions);

0 commit comments

Comments
 (0)