Skip to content

Commit 77d7a0e

Browse files
committed
GH-2583 - Improve relationship mapping.
Accept maximum of two iterations for a relationship before it gets removed from the candidate list. Closes #2583
1 parent 785dde7 commit 77d7a0e

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed

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

+28
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,13 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
615615
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
616616
if (targetIdSelector.apply(possibleRelationship) == targetNodeId) {
617617

618+
// Reduce the amount of relationships in the candidate list.
619+
// If this relationship got processed twice (OUTGOING, INCOMING), it is never needed again
620+
// and therefor should not be in the list.
621+
// Otherwise, for highly linked data it could potentially cause a StackOverflowError.
622+
if (knownObjects.hasProcessedRelationshipCompletely(possibleRelationship.id())) {
623+
relationshipsFromResult.remove(possibleRelationship);
624+
}
618625
// If the target is the same(equal) node, get the related object from the cache.
619626
// Avoiding the call to the map method also breaks an endless cycle of trying to finish
620627
// the property population of _this_ object.
@@ -777,6 +784,8 @@ static class KnownObjects {
777784
private final Set<Long> previousRecords = new HashSet<>();
778785
private final Set<Long> idsInCreation = new HashSet<>();
779786

787+
private final Map<Long, Integer> processedRelationships = new HashMap<>();
788+
780789
private void storeObject(@Nullable Long internalId, Object object) {
781790
if (internalId == null) {
782791
return;
@@ -863,6 +872,25 @@ private boolean alreadyMappedInPreviousRecord(@Nullable Long internalId) {
863872
}
864873
}
865874

875+
private boolean hasProcessedRelationshipCompletely(Long relationshipId) {
876+
try {
877+
write.lock();
878+
read.lock();
879+
880+
int amount = processedRelationships.computeIfAbsent(relationshipId, s -> 0);
881+
if (amount == 2) {
882+
return true;
883+
}
884+
885+
processedRelationships.put(relationshipId, amount + 1);
886+
return false;
887+
888+
} finally {
889+
write.unlock();
890+
read.unlock();
891+
}
892+
}
893+
866894
/**
867895
* Mark all currently existing objects as mapped.
868896
*/

src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java

+29
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import org.springframework.context.annotation.ComponentScan;
5454
import org.springframework.context.annotation.Configuration;
5555
import org.springframework.dao.DataIntegrityViolationException;
56+
import org.springframework.data.domain.Page;
57+
import org.springframework.data.domain.PageRequest;
5658
import org.springframework.data.domain.Sort;
5759
import org.springframework.data.mapping.PersistentPropertyAccessor;
5860
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
@@ -123,6 +125,8 @@
123125
import org.springframework.data.neo4j.integration.issues.gh2579.TableAndColumnRelation;
124126
import org.springframework.data.neo4j.integration.issues.gh2579.TableNode;
125127
import org.springframework.data.neo4j.integration.issues.gh2579.TableRepository;
128+
import org.springframework.data.neo4j.integration.issues.gh2583.GH2583Node;
129+
import org.springframework.data.neo4j.integration.issues.gh2583.GH2583Repository;
126130
import org.springframework.data.neo4j.integration.misc.ConcreteImplementationTwo;
127131
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
128132
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -169,6 +173,7 @@ protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) {
169173
setupGH2328(transaction);
170174
setupGH2459(transaction);
171175
setupGH2572(transaction);
176+
setupGH2583(transaction);
172177

173178
transaction.commit();
174179
}
@@ -217,6 +222,21 @@ private static void setupGH2459(QueryRunner queryRunner) {
217222
CREATE (po1)-[:hasPet]->(a3)""").consume();
218223
}
219224

225+
private static void setupGH2583(QueryRunner queryRunner) {
226+
queryRunner.run("""
227+
CREATE (n:GH2583Node)-[:LINKED]->(m:GH2583Node)-[:LINKED]->(n)-[:LINKED]->(m)
228+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
229+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
230+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
231+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
232+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
233+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
234+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
235+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
236+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)
237+
-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)""").consume();
238+
}
239+
220240
@BeforeEach
221241
protected void prepareIndividual(
222242
@Autowired CityModelRepository cityModelRepository
@@ -907,6 +927,15 @@ void unwindWithMergeShouldWork(@Autowired Neo4jTemplate template, @Autowired Tab
907927
});
908928
}
909929

930+
@Test
931+
@Tag("GH-2583")
932+
void mapStandardCustomQueryWithLotsOfRelationshipsProperly(@Autowired GH2583Repository repository) {
933+
Page<GH2583Node> nodePage = repository.getNodesByCustomQuery(PageRequest.of(0, 300));
934+
935+
List<GH2583Node> nodes = nodePage.getContent();
936+
assertThat(nodes).hasSize(2);
937+
}
938+
910939
@Configuration
911940
@EnableTransactionManagement
912941
@EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2011-2022 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.integration.issues.gh2583;
17+
18+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
19+
import org.springframework.data.neo4j.core.schema.Id;
20+
import org.springframework.data.neo4j.core.schema.Node;
21+
import org.springframework.data.neo4j.core.schema.Relationship;
22+
23+
import java.util.List;
24+
25+
/**
26+
* A simple node with bidirectional relationship mapping to the very same type.
27+
*/
28+
@Node
29+
public class GH2583Node {
30+
@Id
31+
@GeneratedValue
32+
Long id;
33+
34+
@Relationship(type = "LINKED", direction = Relationship.Direction.OUTGOING)
35+
public List<GH2583Node> outgoingNodes;
36+
37+
@Relationship(type = "LINKED", direction = Relationship.Direction.INCOMING)
38+
public List<GH2583Node> incomingNodes;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2011-2022 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.integration.issues.gh2583;
17+
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.Pageable;
20+
import org.springframework.data.neo4j.repository.Neo4jRepository;
21+
import org.springframework.data.neo4j.repository.query.Query;
22+
23+
/**
24+
* Repository with custom query that uncovers a possible StackOverflowError.
25+
*/
26+
public interface GH2583Repository extends Neo4jRepository<GH2583Node, Long> {
27+
28+
@Query(value = "MATCH (s:GH2583Node) " +
29+
"WITH s OPTIONAL MATCH (s)-[r:LINKED]->(t:GH2583Node) " +
30+
"RETURN s, collect(r), collect(t) " +
31+
":#{orderBy(#pageable)} SKIP $skip LIMIT $limit",
32+
countQuery = "MATCH (s:hktxjm) RETURN count(s)")
33+
Page<GH2583Node> getNodesByCustomQuery(Pageable pageable);
34+
}

0 commit comments

Comments
 (0)