Skip to content

Commit 2561524

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 79902fa commit 2561524

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-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
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.neo4j.driver.Driver;
21+
import org.neo4j.driver.Session;
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.data.domain.Page;
26+
import org.springframework.data.domain.PageRequest;
27+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
28+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
29+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
30+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
31+
import org.springframework.data.neo4j.test.BookmarkCapture;
32+
import org.springframework.data.neo4j.test.Neo4jExtension;
33+
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
34+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
35+
import org.springframework.transaction.PlatformTransactionManager;
36+
import org.springframework.transaction.annotation.EnableTransactionManagement;
37+
38+
import java.util.Collection;
39+
import java.util.Collections;
40+
import java.util.List;
41+
42+
import static org.assertj.core.api.Assertions.assertThat;
43+
44+
/**
45+
* Integration test to ensure that also backward updated through the result set work without
46+
* running into a StackOverflowError.
47+
*/
48+
@Neo4jIntegrationTest
49+
public class GH2583IT {
50+
51+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
52+
53+
@BeforeEach
54+
void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {
55+
56+
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
57+
session.run("MATCH (n) DETACH DELETE n").consume();
58+
session.run("CREATE (n:GH2583Node)-[:LINKED]->(m:GH2583Node)-[:LINKED]->(n)-[:LINKED]->(m)" +
59+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
60+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
61+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
62+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
63+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
64+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
65+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
66+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
67+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)" +
68+
"-[:LINKED]->(n)-[:LINKED]->(m)-[:LINKED]->(n)-[:LINKED]->(m)").consume();
69+
bookmarkCapture.seedWith(session.lastBookmark());
70+
}
71+
}
72+
73+
@Test
74+
void mapStandardCustomQueryWithLotsOfRelationshipsProperly(@Autowired GH2583Repository repository) {
75+
Page<GH2583Node> nodePage = repository.getNodesByCustomQuery(PageRequest.of(0, 300));
76+
77+
List<GH2583Node> nodes = nodePage.getContent();
78+
assertThat(nodes).hasSize(2);
79+
}
80+
81+
@Configuration
82+
@EnableTransactionManagement
83+
@EnableNeo4jRepositories(considerNestedRepositories = true)
84+
static class Config extends Neo4jImperativeTestConfiguration {
85+
86+
@Bean
87+
public BookmarkCapture bookmarkCapture() {
88+
return new BookmarkCapture();
89+
}
90+
91+
@Override
92+
public PlatformTransactionManager transactionManager(
93+
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
94+
95+
BookmarkCapture bookmarkCapture = bookmarkCapture();
96+
return new Neo4jTransactionManager(driver, databaseNameProvider,
97+
Neo4jBookmarkManager.create(bookmarkCapture));
98+
}
99+
100+
@Override
101+
protected Collection<String> getMappingBasePackages() {
102+
return Collections.singleton(GH2583Node.class.getPackage().getName());
103+
}
104+
105+
@Bean
106+
public Driver driver() {
107+
108+
return neo4jConnectionSupport.getDriver();
109+
}
110+
111+
@Override
112+
public boolean isCypher5Compatible() {
113+
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
114+
}
115+
}
116+
}
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,31 @@
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+
interface GH2583Repository extends Neo4jRepository<GH2583Node, Long> {
24+
25+
@Query(value = "MATCH (s:GH2583Node) " +
26+
"WITH s OPTIONAL MATCH (s)-[r:LINKED]->(t:GH2583Node) " +
27+
"RETURN s, collect(r), collect(t) " +
28+
":#{orderBy(#pageable)} SKIP $skip LIMIT $limit",
29+
countQuery = "MATCH (s:hktxjm) RETURN count(s)")
30+
Page<GH2583Node> getNodesByCustomQuery(Pageable pageable);
31+
}

0 commit comments

Comments
 (0)