Skip to content

Commit f488e24

Browse files
committed
GH-2583 - Improve relationship mapping.
Accept maximum of two iterations for a relationship before it gets removed from the candidate list.
1 parent 36e7a66 commit f488e24

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

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

Lines changed: 28 additions & 0 deletions
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
*/
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.springframework.data.neo4j.integration.issues.gh2583;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.neo4j.driver.Driver;
6+
import org.neo4j.driver.Session;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.data.domain.Page;
11+
import org.springframework.data.domain.PageRequest;
12+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
13+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
14+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
15+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
16+
import org.springframework.data.neo4j.test.BookmarkCapture;
17+
import org.springframework.data.neo4j.test.Neo4jExtension;
18+
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
19+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
20+
import org.springframework.transaction.PlatformTransactionManager;
21+
import org.springframework.transaction.annotation.EnableTransactionManagement;
22+
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.List;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
@Neo4jIntegrationTest
30+
public class GH2583IT {
31+
32+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
33+
34+
@BeforeEach
35+
void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {
36+
37+
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
38+
session.run("MATCH (n) DETACH DELETE n").consume();
39+
session.run("CREATE (n:GH2583Node)-[:LINKED]->(m:GH2583Node)-[:LINKED]->(n)-[:LINKED]->(m)" +
40+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
41+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
42+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
43+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
44+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
45+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
46+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
47+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
48+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
49+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)").consume();
50+
bookmarkCapture.seedWith(session.lastBookmark());
51+
}
52+
}
53+
54+
@Test
55+
void mapStandardCustomQueryWithLotsOfRelationshipsProperly(@Autowired GH2583Repository repository) {
56+
Page<GH2583Node> nodePage = repository.getNodesByCustomQuery(PageRequest.of(0, 300));
57+
58+
List<GH2583Node> nodes = nodePage.getContent();
59+
assertThat(nodes).hasSize(2);
60+
}
61+
62+
@Configuration
63+
@EnableTransactionManagement
64+
@EnableNeo4jRepositories(considerNestedRepositories = true)
65+
static class Config extends Neo4jImperativeTestConfiguration {
66+
67+
@Bean
68+
public BookmarkCapture bookmarkCapture() {
69+
return new BookmarkCapture();
70+
}
71+
72+
@Override
73+
public PlatformTransactionManager transactionManager(
74+
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
75+
76+
BookmarkCapture bookmarkCapture = bookmarkCapture();
77+
return new Neo4jTransactionManager(driver, databaseNameProvider,
78+
Neo4jBookmarkManager.create(bookmarkCapture));
79+
}
80+
81+
@Override
82+
protected Collection<String> getMappingBasePackages() {
83+
return Collections.singleton(GH2583Node.class.getPackage().getName());
84+
}
85+
86+
@Bean
87+
public Driver driver() {
88+
89+
return neo4jConnectionSupport.getDriver();
90+
}
91+
92+
@Override
93+
public boolean isCypher5Compatible() {
94+
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
95+
}
96+
}
97+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.springframework.data.neo4j.integration.issues.gh2583;
2+
3+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
4+
import org.springframework.data.neo4j.core.schema.Id;
5+
import org.springframework.data.neo4j.core.schema.Node;
6+
import org.springframework.data.neo4j.core.schema.Relationship;
7+
8+
import java.util.List;
9+
10+
/**
11+
* A simple node with bidirectional relationship mapping to the very same type.
12+
*/
13+
@Node
14+
public class GH2583Node {
15+
@Id
16+
@GeneratedValue
17+
Long id;
18+
19+
@Relationship(type = "LINKED", direction = Relationship.Direction.OUTGOING)
20+
public List<GH2583Node> outgoingNodes;
21+
22+
@Relationship(type = "LINKED", direction = Relationship.Direction.INCOMING)
23+
public List<GH2583Node> incomingNodes;
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.springframework.data.neo4j.integration.issues.gh2583;
2+
3+
import org.springframework.data.domain.Page;
4+
import org.springframework.data.domain.Pageable;
5+
import org.springframework.data.neo4j.repository.Neo4jRepository;
6+
import org.springframework.data.neo4j.repository.query.Query;
7+
8+
public interface GH2583Repository extends Neo4jRepository<GH2583Node, Long> {
9+
10+
@Query(value = "MATCH (s:GH2583Node) " +
11+
"WITH s OPTIONAL MATCH (s)-[r:LINKED]->(t:GH2583Node) " +
12+
"RETURN s, collect(r), collect(t) " +
13+
":#{orderBy(#pageable)} SKIP $skip LIMIT $limit",
14+
countQuery = "MATCH (s:hktxjm) RETURN count(s)")
15+
Page<GH2583Node> getNodesByCustomQuery(Pageable pageable);
16+
}

0 commit comments

Comments
 (0)