diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index 8fb22cec3..470724ea3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -79,6 +79,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private final Type relationshipType; private final Type mapType; private final Type listType; + private final Map> labelNodeCache = new HashMap<>(); DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService, NodeDescriptionStore nodeDescriptionStore, TypeSystem typeSystem) { @@ -103,6 +104,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Neo4jPersistentEntity rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType); knownObjects.nextRecord(); + labelNodeCache.clear(); MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription); if (queryRoot == null) { @@ -644,10 +646,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private Collection extractMatchingNodes(Collection allNodesInResult, String targetLabel) { - Predicate onlyWithMatchingLabels = n -> n.hasLabel(targetLabel); - return allNodesInResult.stream() - .filter(onlyWithMatchingLabels) - .collect(Collectors.toList()); + return labelNodeCache.computeIfAbsent(targetLabel, (label) -> { + + Predicate onlyWithMatchingLabels = n -> n.hasLabel(label); + return allNodesInResult.stream() + .filter(onlyWithMatchingLabels) + .collect(Collectors.toList()); + }); } private Collection extractNodes(MapAccessor allValues) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index 8640b61cb..468625e80 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -85,6 +85,8 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity isRelationshipPropertiesEntity; + private final Lazy>> childNodeDescriptionsInHierarchy; + DefaultNeo4jPersistentEntity(TypeInformation information) { super(information); @@ -95,6 +97,7 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity isAnnotationPresent(RelationshipProperties.class)); this.idDescription = Lazy.of(this::computeIdDescription); + this.childNodeDescriptionsInHierarchy = Lazy.of(this::computeChildNodeDescriptionInHierarchy); } /* @@ -513,6 +516,10 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> getChildNodeDescriptionsInHierarchy() { + return childNodeDescriptionsInHierarchy.get(); + } + + private Set> computeChildNodeDescriptionInHierarchy() { Set> childNodes = new HashSet<>(childNodeDescriptions); for (NodeDescription childNodeDescription : childNodeDescriptions) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java index 1b6acedda..f1860dee2 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NodeDescriptionStore.java @@ -18,15 +18,12 @@ package org.springframework.data.neo4j.core.mapping; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.function.BiFunction; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.lang.Nullable; @@ -46,6 +43,24 @@ final class NodeDescriptionStore { */ private final Map> nodeDescriptionsByPrimaryLabel = new HashMap<>(); + private final Map, Map, NodeDescriptionAndLabels>> nodeDescriptionAndLabelsCache = new HashMap<>(); + + private final BiFunction, List, NodeDescriptionAndLabels> nodeDescriptionAndLabels = + (nodeDescription, labels) -> { + Map, NodeDescriptionAndLabels> listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription); + if (listNodeDescriptionAndLabelsMap == null) { + nodeDescriptionAndLabelsCache.put(nodeDescription, new HashMap<>()); + listNodeDescriptionAndLabelsMap = nodeDescriptionAndLabelsCache.get(nodeDescription); + } + + NodeDescriptionAndLabels cachedNodeDescriptionAndLabels = listNodeDescriptionAndLabelsMap.get(labels); + if (cachedNodeDescriptionAndLabels == null) { + cachedNodeDescriptionAndLabels = computeConcreteNodeDescription(nodeDescription, labels); + listNodeDescriptionAndLabelsMap.put(labels, cachedNodeDescriptionAndLabels); + } + return cachedNodeDescriptionAndLabels; + }; + public boolean containsKey(String primaryLabel) { return nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel); } @@ -81,7 +96,11 @@ final class NodeDescriptionStore { return null; } - public NodeDescriptionAndLabels deriveConcreteNodeDescription(Neo4jPersistentEntity entityDescription, List labels) { + public NodeDescriptionAndLabels deriveConcreteNodeDescription(NodeDescription entityDescription, List labels) { + return nodeDescriptionAndLabels.apply(entityDescription, labels); + } + + private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription entityDescription, List labels) { boolean isConcreteClassThatFulfillsEverything = !Modifier.isAbstract(entityDescription.getUnderlyingClass().getModifiers()) && entityDescription.getStaticLabels().containsAll(labels); @@ -97,25 +116,48 @@ final class NodeDescriptionStore { } if (!haystack.isEmpty()) { - Function, Integer> count = (nodeDescription) -> Math.toIntExact(nodeDescription.getStaticLabels().stream().filter(labels::contains).count()); - Optional, Integer>> mostMatchingNodeDescription = haystack.stream() - .filter(nd -> labels.containsAll(nd.getStaticLabels())) // remove candidates having more mandatory labels - .collect(Collectors.toMap(Function.identity(), nodeDescription -> count.apply(nodeDescription))) - .entrySet().stream() - .max(Comparator.comparingInt(Map.Entry::getValue)); - if (mostMatchingNodeDescription.isPresent()) { - NodeDescription childNodeDescription = mostMatchingNodeDescription.get().getKey(); - List staticLabels = childNodeDescription.getStaticLabels(); - Set surplusLabels = new HashSet<>(labels); - surplusLabels.removeAll(staticLabels); - return new NodeDescriptionAndLabels(childNodeDescription, surplusLabels); + NodeDescription mostMatchingNodeDescription = null; + Map, Integer> unmatchedLabelsCache = new HashMap<>(); + List mostMatchingStaticLabels = null; + + // Remove is faster than "stream, filter, count". + BiFunction, List, Integer> unmatchedLabelsCount = + (nodeDescription, staticLabels) -> { + Set staticLabelsClone = new HashSet<>(staticLabels); + labels.forEach(staticLabelsClone::remove); + return staticLabelsClone.size(); + }; + + for (NodeDescription nd : haystack) { + List staticLabels = nd.getStaticLabels(); + + if (staticLabels.containsAll(labels)) { + Set surplusLabels = new HashSet<>(labels); + staticLabels.forEach(surplusLabels::remove); + return new NodeDescriptionAndLabels(nd, surplusLabels); + } + + unmatchedLabelsCache.put(nd, unmatchedLabelsCount.apply(nd, staticLabels)); + if (mostMatchingNodeDescription == null) { + mostMatchingNodeDescription = nd; + mostMatchingStaticLabels = staticLabels; + continue; + } + + if (unmatchedLabelsCache.get(nd) < unmatchedLabelsCache.get(mostMatchingNodeDescription)) { + mostMatchingNodeDescription = nd; + } } + + Set surplusLabels = new HashSet<>(labels); + mostMatchingStaticLabels.forEach(surplusLabels::remove); + return new NodeDescriptionAndLabels(mostMatchingNodeDescription, surplusLabels); } Set surplusLabels = new HashSet<>(labels); surplusLabels.remove(entityDescription.getPrimaryLabel()); - surplusLabels.removeAll(entityDescription.getAdditionalLabels()); + entityDescription.getAdditionalLabels().forEach(surplusLabels::remove); return new NodeDescriptionAndLabels(entityDescription, surplusLabels); } }