Skip to content

Commit 6af31fd

Browse files
committed
GH-2320 - Introduce support for cyclic projections.
Closes #2320
1 parent f4f0058 commit 6af31fd

20 files changed

+316
-153
lines changed

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.springframework.core.log.LogAccessor;
5858
import org.springframework.dao.IncorrectResultSizeDataAccessException;
5959
import org.springframework.dao.OptimisticLockingFailureException;
60+
import org.springframework.data.mapping.Association;
6061
import org.springframework.data.mapping.AssociationHandler;
6162
import org.springframework.data.mapping.PersistentPropertyAccessor;
6263
import org.springframework.data.mapping.PropertyPath;
@@ -79,6 +80,7 @@
7980
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
8081
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
8182
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
83+
import org.springframework.data.neo4j.core.schema.TargetNode;
8284
import org.springframework.data.neo4j.repository.NoResultException;
8385
import org.springframework.data.neo4j.repository.query.QueryFragments;
8486
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -318,7 +320,7 @@ private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Obj
318320
@Override
319321
public <T> T save(T instance) {
320322

321-
return saveImpl(instance, Collections.emptyList(), null);
323+
return saveImpl(instance, Collections.emptyMap(), null);
322324
}
323325

324326
@Override
@@ -335,7 +337,7 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
335337
}
336338

337339
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
338-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
340+
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
339341
projectionFactory, neo4jMappingContext);
340342

341343
T savedInstance = saveImpl(instance, pps, null);
@@ -350,7 +352,7 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
350352
this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass()).get());
351353
}
352354

353-
private <T> T saveImpl(T instance, @Nullable Collection<PropertyPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
355+
private <T> T saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
354356

355357
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
356358
return instance;
@@ -438,10 +440,10 @@ private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersist
438440

439441
@Override
440442
public <T> List<T> saveAll(Iterable<T> instances) {
441-
return saveAllImpl(instances, Collections.emptyList());
443+
return saveAllImpl(instances, Collections.emptyMap());
442444
}
443445

444-
private <T> List<T> saveAllImpl(Iterable<T> instances, List<PropertyPath> includedProperties) {
446+
private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean> includedProperties) {
445447

446448
Set<Class<?>> types = new HashSet<>();
447449
List<T> entities = new ArrayList<>();
@@ -520,10 +522,10 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
520522

521523
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
522524

523-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
525+
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
524526
projectionFactory, neo4jMappingContext);
525527

526-
List<T> savedInstances = saveAllImpl(instances, new ArrayList<>(pps));
528+
List<T> savedInstances = saveAllImpl(instances, pps);
527529

528530
if (projectionInformation.isClosed()) {
529531
return savedInstances.stream().map(instance -> projectionFactory.createProjection(resultType, instance))
@@ -930,7 +932,7 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
930932

931933
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
932934

933-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
935+
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
934936
projectionFactory, neo4jMappingContext);
935937

936938
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
@@ -1061,16 +1063,34 @@ private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsBy
10611063
.bindAll(parameters)
10621064
.fetch()
10631065
.one()
1064-
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription));
1066+
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, PropertyPathWalkStep.empty()));
10651067
}
10661068

10671069
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
10681070
}
10691071

1070-
private void iterateNextLevel(Collection<Long> nodeIds, Neo4jPersistentEntity<?> target, Set<Long> relationshipIds,
1071-
Set<Long> relatedNodeIds) {
1072+
private void iterateNextLevel(Collection<Long> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<Long> relationshipIds,
1073+
Set<Long> relatedNodeIds, PropertyPathWalkStep currentPathStep) {
1074+
1075+
Neo4jPersistentEntity<?> target = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getTarget();
1076+
1077+
@SuppressWarnings("unchecked")
1078+
String fieldName = ((Association<Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
1079+
PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ?
1080+
fieldName + "." + ((Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRelationshipPropertiesEntity())
1081+
.getPersistentProperty(TargetNode.class).getFieldName() : fieldName));
1082+
1083+
1084+
Collection<RelationshipDescription> relationships = target
1085+
.getRelationshipsInHierarchy(
1086+
relaxedPropertyPath -> {
1087+
1088+
PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path);
1089+
prepend = PropertyFilter.RelaxedPropertyPath.withRootType(preparedQuery.getResultType()).append(prepend.toDotPath());
1090+
return preparedQuery.getQueryFragmentsAndParameters().getQueryFragments().includeField(prepend);
1091+
}
1092+
);
10721093

1073-
Collection<RelationshipDescription> relationships = target.getRelationshipsInHierarchy(preparedQuery.getQueryFragmentsAndParameters().getQueryFragments()::includeField);
10741094
for (RelationshipDescription relationshipDescription : relationships) {
10751095

10761096
Node node = anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(target));
@@ -1084,13 +1104,15 @@ private void iterateNextLevel(Collection<Long> nodeIds, Neo4jPersistentEntity<?>
10841104
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, nodeIds))
10851105
.fetch()
10861106
.one()
1087-
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription));
1107+
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, nextPathStep));
10881108
}
10891109
}
10901110

10911111
@NonNull
10921112
private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relationshipIds,
1093-
Set<Long> relatedNodeIds, RelationshipDescription relationshipDescription) {
1113+
Set<Long> relatedNodeIds,
1114+
RelationshipDescription relationshipDescription,
1115+
PropertyPathWalkStep currentPathStep) {
10941116

10951117
return record -> {
10961118
@SuppressWarnings("unchecked")
@@ -1107,8 +1129,7 @@ private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relations
11071129
relatedNodeIds.addAll(relatedIds);
11081130
// 2. for the rest start the exploration
11091131
if (!relatedIds.isEmpty()) {
1110-
iterateNextLevel(relatedIds, (Neo4jPersistentEntity<?>) relationshipDescription.getTarget(),
1111-
relationshipIds, relatedNodeIds);
1132+
iterateNextLevel(relatedIds, relationshipDescription, relationshipIds, relatedNodeIds, currentPathStep);
11121133
}
11131134
};
11141135
}

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

Lines changed: 25 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
import org.springframework.data.repository.query.ReturnedType;
2727

2828
import java.beans.PropertyDescriptor;
29-
import java.util.ArrayList;
3029
import java.util.Collection;
3130
import java.util.Collections;
31+
import java.util.HashMap;
3232
import java.util.HashSet;
33-
import java.util.List;
33+
import java.util.Map;
3434

3535
/**
3636
* This class is responsible for creating a List of {@link PropertyPath} entries that contains all reachable
@@ -39,17 +39,17 @@
3939
@API(status = API.Status.INTERNAL, since = "6.1.3")
4040
public final class PropertyFilterSupport {
4141

42-
public static List<PropertyPath> getInputProperties(ResultProcessor resultProcessor, ProjectionFactory factory,
43-
Neo4jMappingContext mappingContext) {
42+
public static Map<PropertyPath, Boolean> getInputProperties(ResultProcessor resultProcessor, ProjectionFactory factory,
43+
Neo4jMappingContext mappingContext) {
4444

4545
ReturnedType returnedType = resultProcessor.getReturnedType();
46-
List<PropertyPath> filteredProperties = new ArrayList<>();
46+
Map<PropertyPath, Boolean> filteredProperties = new HashMap<>();
4747

4848
boolean isProjecting = returnedType.isProjecting();
4949
boolean isClosedProjection = factory.getProjectionInformation(returnedType.getReturnedType()).isClosed();
5050

5151
if (!isProjecting || !isClosedProjection) {
52-
return Collections.emptyList();
52+
return Collections.emptyMap();
5353
}
5454

5555
for (String inputProperty : returnedType.getInputProperties()) {
@@ -60,21 +60,21 @@ public static List<PropertyPath> getInputProperties(ResultProcessor resultProces
6060
return filteredProperties;
6161
}
6262

63-
static List<PropertyPath> addPropertiesFrom(Class<?> domainType, Class<?> returnType,
63+
static Map<PropertyPath, Boolean> addPropertiesFrom(Class<?> domainType, Class<?> returnType,
6464
ProjectionFactory projectionFactory,
6565
Neo4jMappingContext neo4jMappingContext) {
6666

6767
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
68-
List<PropertyPath> propertyPaths = new ArrayList<>();
68+
Map<PropertyPath, Boolean> propertyPaths = new HashMap<>();
6969
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
7070
addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, inputProperty.getName(), neo4jMappingContext);
7171
}
7272
return propertyPaths;
7373
}
7474

7575
private static void addPropertiesFrom(Class<?> domainType, Class<?> returnedType, ProjectionFactory factory,
76-
Collection<PropertyPath> filteredProperties, String inputProperty,
77-
Neo4jMappingContext mappingContext) {
76+
Map<PropertyPath, Boolean> filteredProperties, String inputProperty,
77+
Neo4jMappingContext mappingContext) {
7878

7979
ProjectionInformation projectionInformation = factory.getProjectionInformation(returnedType);
8080
PropertyPath propertyPath;
@@ -93,33 +93,33 @@ private static void addPropertiesFrom(Class<?> domainType, Class<?> returnedType
9393
// 2. Something that looks like an entity needs to get processed as such
9494
// 3. Embedded projection
9595
if (mappingContext.getConversionService().isSimpleType(propertyType)) {
96-
filteredProperties.add(propertyPath);
96+
filteredProperties.put(propertyPath, false);
9797
} else if (mappingContext.hasPersistentEntityFor(propertyType)) {
98-
// avoid recursion / cycles
99-
if (propertyType.equals(domainType)) {
100-
return;
101-
}
102-
10398
addPropertiesFromEntity(filteredProperties, propertyPath, propertyType, mappingContext, new HashSet<>());
10499
} else {
105100
ProjectionInformation nestedProjectionInformation = factory.getProjectionInformation(propertyType);
106-
filteredProperties.add(propertyPath);
107101
// Closed projection should get handled as above (recursion)
108102
if (nestedProjectionInformation.isClosed()) {
103+
filteredProperties.put(propertyPath, false);
109104
for (PropertyDescriptor nestedInputProperty : nestedProjectionInformation.getInputProperties()) {
110105
PropertyPath nestedPropertyPath = propertyPath.nested(nestedInputProperty.getName());
111-
filteredProperties.add(nestedPropertyPath);
106+
if (propertyPath.hasNext() && (domainType.equals(propertyPath.getLeafProperty().getOwningType().getType())
107+
|| returnedType.equals(propertyPath.getLeafProperty().getOwningType().getType()))) {
108+
break;
109+
}
110+
112111
addPropertiesFrom(domainType, returnedType, factory, filteredProperties,
113112
nestedPropertyPath.toDotPath(), mappingContext);
114113
}
115114
} else {
116115
// an open projection at this place needs to get replaced with the matching (real) entity
116+
filteredProperties.put(propertyPath, true);
117117
processEntity(domainType, filteredProperties, inputProperty, mappingContext);
118118
}
119119
}
120120
}
121121

122-
private static void processEntity(Class<?> domainType, Collection<PropertyPath> filteredProperties,
122+
private static void processEntity(Class<?> domainType, Map<PropertyPath, Boolean> filteredProperties,
123123
String inputProperty, Neo4jMappingContext mappingContext) {
124124

125125
Neo4jPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainType);
@@ -131,59 +131,27 @@ private static void processEntity(Class<?> domainType, Collection<PropertyPath>
131131
addPropertiesFromEntity(filteredProperties, propertyPath, propertyEntityType, mappingContext, new HashSet<>());
132132
}
133133

134-
private static void addPropertiesFromEntity(Collection<PropertyPath> filteredProperties, PropertyPath propertyPath,
134+
private static void addPropertiesFromEntity(Map<PropertyPath, Boolean> filteredProperties, PropertyPath propertyPath,
135135
Class<?> propertyType, Neo4jMappingContext mappingContext,
136136
Collection<Neo4jPersistentEntity<?>> processedEntities) {
137137

138+
if (!mappingContext.hasPersistentEntityFor(propertyType)) {
139+
throw new RuntimeException("hmmmm");
140+
}
141+
138142
Neo4jPersistentEntity<?> persistentEntityFromProperty = mappingContext.getPersistentEntity(propertyType);
139143
// break the recursion / cycles
140144
if (hasProcessedEntity(persistentEntityFromProperty, processedEntities)) {
141145
return;
142146
}
143-
processedEntities.add(persistentEntityFromProperty);
144147

145-
// save base/root entity/projection type to avoid recursion later
146-
Class<?> pathRootType = propertyPath.getOwningType().getType();
147-
if (mappingContext.hasPersistentEntityFor(pathRootType)) {
148-
processedEntities.add(mappingContext.getPersistentEntity(pathRootType));
149-
}
148+
filteredProperties.put(propertyPath, true);
150149

151-
takeAllPropertiesFromEntity(filteredProperties, propertyPath, mappingContext, persistentEntityFromProperty, processedEntities);
152150
}
153151

154152
private static boolean hasProcessedEntity(Neo4jPersistentEntity<?> persistentEntityFromProperty,
155153
Collection<Neo4jPersistentEntity<?>> processedEntities) {
156154

157155
return processedEntities.contains(persistentEntityFromProperty);
158156
}
159-
160-
private static void takeAllPropertiesFromEntity(Collection<PropertyPath> filteredProperties,
161-
PropertyPath propertyPath, Neo4jMappingContext mappingContext,
162-
Neo4jPersistentEntity<?> persistentEntityFromProperty,
163-
Collection<Neo4jPersistentEntity<?>> processedEntities) {
164-
165-
filteredProperties.add(propertyPath);
166-
167-
persistentEntityFromProperty.doWithAll(neo4jPersistentProperty -> {
168-
addPropertiesFromEntity(filteredProperties, propertyPath.nested(neo4jPersistentProperty.getFieldName()), mappingContext, processedEntities);
169-
});
170-
}
171-
172-
private static void addPropertiesFromEntity(Collection<PropertyPath> filteredProperties, PropertyPath propertyPath,
173-
Neo4jMappingContext mappingContext,
174-
Collection<Neo4jPersistentEntity<?>> processedEntities) {
175-
176-
// break the recursion / cycles
177-
if (filteredProperties.contains(propertyPath)) {
178-
return;
179-
}
180-
Class<?> propertyType = propertyPath.getLeafType();
181-
// simple types can get added directly to the list.
182-
if (mappingContext.getConversionService().isSimpleType(propertyType)) {
183-
filteredProperties.add(propertyPath);
184-
// Other types are handled also as entities because there cannot be any nested projection within a real entity.
185-
} else if (mappingContext.hasPersistentEntityFor(propertyType)) {
186-
addPropertiesFromEntity(filteredProperties, propertyPath, propertyType, mappingContext, processedEntities);
187-
}
188-
}
189157
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2011-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core;
17+
18+
import org.apiguardian.api.API;
19+
20+
/**
21+
* Wrapper class for simple propertyPath specific modification.
22+
* Returns new instances on modification and hides the ugly empty String.
23+
*/
24+
@API(status = API.Status.INTERNAL)
25+
class PropertyPathWalkStep {
26+
27+
final String path;
28+
29+
static PropertyPathWalkStep empty() {
30+
return new PropertyPathWalkStep("");
31+
}
32+
33+
public PropertyPathWalkStep with(String addition) {
34+
return new PropertyPathWalkStep(path.isEmpty() ? addition : path + "." + addition);
35+
}
36+
37+
private PropertyPathWalkStep(String path) {
38+
this.path = path;
39+
}
40+
}

0 commit comments

Comments
 (0)