Skip to content

Commit 1c2ef53

Browse files
committed
GH-2487 - Improve performance of NodeDescription determination.
Closes #2487
1 parent ace54d7 commit 1c2ef53

File tree

3 files changed

+78
-23
lines changed

3 files changed

+78
-23
lines changed

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
7878
private final Type relationshipType;
7979
private final Type mapType;
8080
private final Type listType;
81+
private final Map<String, Collection<Node>> labelNodeCache = new HashMap<>();
8182

8283
DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService,
8384
NodeDescriptionStore nodeDescriptionStore, TypeSystem typeSystem) {
@@ -101,6 +102,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
101102
public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
102103

103104
knownObjects.nextRecord();
105+
labelNodeCache.clear();
106+
104107
@SuppressWarnings("unchecked") // ¯\_(ツ)_/¯
105108
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity<R>) nodeDescriptionStore.getNodeDescription(targetType);
106109
MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription);
@@ -658,10 +661,13 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
658661

659662
private Collection<Node> extractMatchingNodes(Collection<Node> allNodesInResult, String targetLabel) {
660663

661-
Predicate<Node> onlyWithMatchingLabels = n -> n.hasLabel(targetLabel);
662-
return allNodesInResult.stream()
663-
.filter(onlyWithMatchingLabels)
664-
.collect(Collectors.toList());
664+
return labelNodeCache.computeIfAbsent(targetLabel, (label) -> {
665+
666+
Predicate<Node> onlyWithMatchingLabels = n -> n.hasLabel(label);
667+
return allNodesInResult.stream()
668+
.filter(onlyWithMatchingLabels)
669+
.collect(Collectors.toList());
670+
});
665671
}
666672

667673
private Collection<Node> extractNodes(MapAccessor allValues) {

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

+7
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
8585

8686
private final Lazy<Boolean> isRelationshipPropertiesEntity;
8787

88+
private final Lazy<List<NodeDescription<?>>> childNodeDescriptionsInHierarchy;
89+
8890
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
8991
super(information);
9092

@@ -95,6 +97,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
9597
.filter(Neo4jPersistentProperty::isDynamicLabels).findFirst().orElse(null));
9698
this.isRelationshipPropertiesEntity = Lazy.of(() -> isAnnotationPresent(RelationshipProperties.class));
9799
this.idDescription = Lazy.of(this::computeIdDescription);
100+
this.childNodeDescriptionsInHierarchy = Lazy.of(this::computeChildNodeDescriptionInHierarchy);
98101
}
99102

100103
/*
@@ -513,6 +516,10 @@ public void addChildNodeDescription(NodeDescription<?> child) {
513516

514517
@Override
515518
public List<NodeDescription<?>> getChildNodeDescriptionsInHierarchy() {
519+
return childNodeDescriptionsInHierarchy.get();
520+
}
521+
522+
private List<NodeDescription<?>> computeChildNodeDescriptionInHierarchy() {
516523
List<NodeDescription<?>> childNodes = new ArrayList<>(childNodeDescriptions);
517524

518525
for (NodeDescription<?> childNodeDescription : childNodeDescriptions) {

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

+61-19
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,12 @@
1818
import java.lang.reflect.Modifier;
1919
import java.util.Collection;
2020
import java.util.Collections;
21-
import java.util.Comparator;
2221
import java.util.HashMap;
2322
import java.util.HashSet;
2423
import java.util.List;
2524
import java.util.Map;
26-
import java.util.Optional;
2725
import java.util.Set;
28-
import java.util.function.Function;
29-
import java.util.stream.Collectors;
26+
import java.util.function.BiFunction;
3027

3128
import org.springframework.data.mapping.context.AbstractMappingContext;
3229
import org.springframework.lang.Nullable;
@@ -46,6 +43,24 @@ final class NodeDescriptionStore {
4643
*/
4744
private final Map<String, NodeDescription<?>> nodeDescriptionsByPrimaryLabel = new HashMap<>();
4845

46+
private final Map<NodeDescription<?>, Map<List<String>, NodeDescriptionAndLabels>> nodeDescriptionAndLabelsCache = new HashMap<>();
47+
48+
private final BiFunction<NodeDescription<?>, List<String>, NodeDescriptionAndLabels> nodeDescriptionAndLabels =
49+
(nodeDescription, labels) -> {
50+
Map<List<String>, NodeDescriptionAndLabels> listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription);
51+
if (listNodeDescriptionAndLabelsMap == null) {
52+
nodeDescriptionAndLabelsCache.put(nodeDescription, new HashMap<>());
53+
listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription);
54+
}
55+
56+
NodeDescriptionAndLabels cachedNodeDescriptionAndLabels = listNodeDescriptionAndLabelsMap.get(labels);
57+
if (cachedNodeDescriptionAndLabels == null) {
58+
cachedNodeDescriptionAndLabels = computeConcreteNodeDescription(nodeDescription, labels);
59+
listNodeDescriptionAndLabelsMap.put(labels, cachedNodeDescriptionAndLabels);
60+
}
61+
return cachedNodeDescriptionAndLabels;
62+
};
63+
4964
public boolean containsKey(String primaryLabel) {
5065
return nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel);
5166
}
@@ -81,7 +96,11 @@ public NodeDescription<?> getNodeDescription(Class<?> targetType) {
8196
return null;
8297
}
8398

84-
public NodeDescriptionAndLabels deriveConcreteNodeDescription(Neo4jPersistentEntity<?> entityDescription, List<String> labels) {
99+
public NodeDescriptionAndLabels deriveConcreteNodeDescription(NodeDescription<?> entityDescription, List<String> labels) {
100+
return nodeDescriptionAndLabels.apply(entityDescription, labels);
101+
}
102+
103+
private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription<?> entityDescription, List<String> labels) {
85104

86105
boolean isConcreteClassThatFulfillsEverything = !Modifier.isAbstract(entityDescription.getUnderlyingClass().getModifiers()) && entityDescription.getStaticLabels().containsAll(labels);
87106

@@ -97,25 +116,48 @@ public NodeDescriptionAndLabels deriveConcreteNodeDescription(Neo4jPersistentEnt
97116
}
98117

99118
if (!haystack.isEmpty()) {
100-
Function<NodeDescription<?>, Integer> count = (nodeDescription) -> Math.toIntExact(nodeDescription.getStaticLabels().stream().filter(labels::contains).count());
101-
Optional<Map.Entry<NodeDescription<?>, Integer>> mostMatchingNodeDescription = haystack.stream()
102-
.filter(nd -> labels.containsAll(nd.getStaticLabels())) // remove candidates having more mandatory labels
103-
.collect(Collectors.toMap(Function.identity(), nodeDescription -> count.apply(nodeDescription)))
104-
.entrySet().stream()
105-
.max(Comparator.comparingInt(Map.Entry::getValue));
106-
107-
if (mostMatchingNodeDescription.isPresent()) {
108-
NodeDescription<?> childNodeDescription = mostMatchingNodeDescription.get().getKey();
109-
List<String> staticLabels = childNodeDescription.getStaticLabels();
110-
Set<String> surplusLabels = new HashSet<>(labels);
111-
surplusLabels.removeAll(staticLabels);
112-
return new NodeDescriptionAndLabels(childNodeDescription, surplusLabels);
119+
120+
NodeDescription<?> mostMatchingNodeDescription = null;
121+
Map<NodeDescription<?>, Integer> unmatchedLabelsCache = new HashMap<>();
122+
List<String> mostMatchingStaticLabels = null;
123+
124+
// Remove is faster than "stream, filter, count".
125+
BiFunction<NodeDescription<?>, List<String>, Integer> unmatchedLabelsCount =
126+
(nodeDescription, staticLabels) -> {
127+
Set<String> staticLabelsClone = new HashSet<>(staticLabels);
128+
labels.forEach(staticLabelsClone::remove);
129+
return staticLabelsClone.size();
130+
};
131+
132+
for (NodeDescription<?> nd : haystack) {
133+
List<String> staticLabels = nd.getStaticLabels();
134+
135+
if (staticLabels.containsAll(labels)) {
136+
Set<String> surplusLabels = new HashSet<>(labels);
137+
staticLabels.forEach(surplusLabels::remove);
138+
return new NodeDescriptionAndLabels(nd, surplusLabels);
139+
}
140+
141+
unmatchedLabelsCache.put(nd, unmatchedLabelsCount.apply(nd, staticLabels));
142+
if (mostMatchingNodeDescription == null) {
143+
mostMatchingNodeDescription = nd;
144+
mostMatchingStaticLabels = staticLabels;
145+
continue;
146+
}
147+
148+
if (unmatchedLabelsCache.get(nd) < unmatchedLabelsCache.get(mostMatchingNodeDescription)) {
149+
mostMatchingNodeDescription = nd;
150+
}
113151
}
152+
153+
Set<String> surplusLabels = new HashSet<>(labels);
154+
mostMatchingStaticLabels.forEach(surplusLabels::remove);
155+
return new NodeDescriptionAndLabels(mostMatchingNodeDescription, surplusLabels);
114156
}
115157

116158
Set<String> surplusLabels = new HashSet<>(labels);
117159
surplusLabels.remove(entityDescription.getPrimaryLabel());
118-
surplusLabels.removeAll(entityDescription.getAdditionalLabels());
160+
entityDescription.getAdditionalLabels().forEach(surplusLabels::remove);
119161
return new NodeDescriptionAndLabels(entityDescription, surplusLabels);
120162
}
121163
}

0 commit comments

Comments
 (0)