Skip to content

Commit 308a5e3

Browse files
committed
GH-2320 - Introduce support for cyclic projections.
Closes #2320
1 parent 2fba435 commit 308a5e3

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
@@ -56,6 +56,7 @@
5656
import org.springframework.core.log.LogAccessor;
5757
import org.springframework.dao.IncorrectResultSizeDataAccessException;
5858
import org.springframework.dao.OptimisticLockingFailureException;
59+
import org.springframework.data.mapping.Association;
5960
import org.springframework.data.mapping.AssociationHandler;
6061
import org.springframework.data.mapping.PersistentPropertyAccessor;
6162
import org.springframework.data.mapping.PropertyPath;
@@ -78,6 +79,7 @@
7879
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
7980
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
8081
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
82+
import org.springframework.data.neo4j.core.schema.TargetNode;
8183
import org.springframework.data.neo4j.repository.NoResultException;
8284
import org.springframework.data.neo4j.repository.query.QueryFragments;
8385
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -316,7 +318,7 @@ private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Obj
316318
@Override
317319
public <T> T save(T instance) {
318320

319-
return saveImpl(instance, Collections.emptyList(), null);
321+
return saveImpl(instance, Collections.emptyMap(), null);
320322
}
321323

322324
@Override
@@ -333,7 +335,7 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
333335
}
334336

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

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

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

353355
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
354356
return instance;
@@ -433,10 +435,10 @@ private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersist
433435

434436
@Override
435437
public <T> List<T> saveAll(Iterable<T> instances) {
436-
return saveAllImpl(instances, Collections.emptyList());
438+
return saveAllImpl(instances, Collections.emptyMap());
437439
}
438440

439-
private <T> List<T> saveAllImpl(Iterable<T> instances, List<PropertyPath> includedProperties) {
441+
private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean> includedProperties) {
440442

441443
Set<Class<?>> types = new HashSet<>();
442444
List<T> entities = new ArrayList<>();
@@ -512,10 +514,10 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
512514

513515
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(resultType);
514516

515-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
517+
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
516518
projectionFactory, neo4jMappingContext);
517519

518-
List<T> savedInstances = saveAllImpl(instances, new ArrayList<>(pps));
520+
List<T> savedInstances = saveAllImpl(instances, pps);
519521

520522
if (projectionInformation.isClosed()) {
521523
return savedInstances.stream().map(instance -> projectionFactory.createProjection(resultType, instance))
@@ -915,7 +917,7 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
915917

916918
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
917919

918-
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
920+
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
919921
projectionFactory, neo4jMappingContext);
920922

921923
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
@@ -1042,16 +1044,34 @@ private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsBy
10421044
.bindAll(parameters)
10431045
.fetch()
10441046
.one()
1045-
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription));
1047+
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, PropertyPathWalkStep.empty()));
10461048
}
10471049

10481050
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
10491051
}
10501052

1051-
private void iterateNextLevel(Collection<Long> nodeIds, Neo4jPersistentEntity<?> target, Set<Long> relationshipIds,
1052-
Set<Long> relatedNodeIds) {
1053+
private void iterateNextLevel(Collection<Long> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<Long> relationshipIds,
1054+
Set<Long> relatedNodeIds, PropertyPathWalkStep currentPathStep) {
1055+
1056+
Neo4jPersistentEntity<?> target = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getTarget();
1057+
1058+
@SuppressWarnings("unchecked")
1059+
String fieldName = ((Association<Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
1060+
PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ?
1061+
fieldName + "." + ((Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRelationshipPropertiesEntity())
1062+
.getPersistentProperty(TargetNode.class).getFieldName() : fieldName));
1063+
1064+
1065+
Collection<RelationshipDescription> relationships = target
1066+
.getRelationshipsInHierarchy(
1067+
relaxedPropertyPath -> {
1068+
1069+
PropertyFilter.RelaxedPropertyPath prepend = relaxedPropertyPath.prepend(nextPathStep.path);
1070+
prepend = PropertyFilter.RelaxedPropertyPath.withRootType(preparedQuery.getResultType()).append(prepend.toDotPath());
1071+
return preparedQuery.getQueryFragmentsAndParameters().getQueryFragments().includeField(prepend);
1072+
}
1073+
);
10531074

1054-
Collection<RelationshipDescription> relationships = target.getRelationshipsInHierarchy(preparedQuery.getQueryFragmentsAndParameters().getQueryFragments()::includeField);
10551075
for (RelationshipDescription relationshipDescription : relationships) {
10561076

10571077
Node node = anyNode(Constants.NAME_OF_ROOT_NODE);
@@ -1065,13 +1085,15 @@ private void iterateNextLevel(Collection<Long> nodeIds, Neo4jPersistentEntity<?>
10651085
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, nodeIds))
10661086
.fetch()
10671087
.one()
1068-
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription));
1088+
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, nextPathStep));
10691089
}
10701090
}
10711091

10721092
@NonNull
10731093
private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relationshipIds,
1074-
Set<Long> relatedNodeIds, RelationshipDescription relationshipDescription) {
1094+
Set<Long> relatedNodeIds,
1095+
RelationshipDescription relationshipDescription,
1096+
PropertyPathWalkStep currentPathStep) {
10751097

10761098
return record -> {
10771099
List<Long> newRelationshipIds = (List<Long>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
@@ -1086,8 +1108,7 @@ private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relations
10861108
relatedNodeIds.addAll(relatedIds);
10871109
// 2. for the rest start the exploration
10881110
if (!relatedIds.isEmpty()) {
1089-
iterateNextLevel(relatedIds, (Neo4jPersistentEntity<?>) relationshipDescription.getTarget(),
1090-
relationshipIds, relatedNodeIds);
1111+
iterateNextLevel(relatedIds, relationshipDescription, relationshipIds, relatedNodeIds, currentPathStep);
10911112
}
10921113
};
10931114
}

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)