Skip to content

Commit 2715591

Browse files
GH-1906 - Ensure that self referential path structures work correctly.
This closes #1906.
1 parent bddfecc commit 2715591

File tree

7 files changed

+328
-74
lines changed

7 files changed

+328
-74
lines changed

src/test/java/org/springframework/data/neo4j/integration/movies/imperative/AdvancedMappingIT.java

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919

20-
import java.io.BufferedReader;
2120
import java.io.IOException;
22-
import java.io.InputStreamReader;
2321
import java.util.Collections;
2422
import java.util.HashMap;
2523
import java.util.List;
2624
import java.util.Map;
25+
import java.util.Optional;
2726
import java.util.function.Function;
2827
import java.util.stream.Collectors;
2928

@@ -37,7 +36,10 @@
3736
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
3837
import org.springframework.data.neo4j.core.Neo4jTemplate;
3938
import org.springframework.data.neo4j.integration.movies.shared.Actor;
39+
import org.springframework.data.neo4j.integration.movies.shared.CypherUtils;
4040
import org.springframework.data.neo4j.integration.movies.shared.Movie;
41+
import org.springframework.data.neo4j.integration.movies.shared.Organisation;
42+
import org.springframework.data.neo4j.integration.movies.shared.Partner;
4143
import org.springframework.data.neo4j.integration.movies.shared.Person;
4244
import org.springframework.data.neo4j.repository.Neo4jRepository;
4345
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@@ -61,37 +63,16 @@ class AdvancedMappingIT {
6163
@BeforeAll
6264
static void setupData(@Autowired Driver driver) throws IOException {
6365

64-
try (BufferedReader moviesReader = new BufferedReader(
65-
new InputStreamReader(AdvancedMappingIT.class.getResourceAsStream("/data/movies.cypher")));
66-
Session session = driver.session()) {
66+
try (Session session = driver.session()) {
6767
session.run("MATCH (n) DETACH DELETE n").consume();
68-
String moviesCypher = moviesReader.lines().collect(Collectors.joining(" "));
69-
session.run(moviesCypher).consume();
70-
session.run("MATCH (l1:Person {name: 'Lilly Wachowski'})\n"
71-
+ "MATCH (l2:Person {name: 'Lana Wachowski'})\n"
72-
+ "CREATE (l1) - [s:IS_SIBLING_OF] -> (l2)\n"
73-
+ "RETURN *").consume();
74-
session.run("MATCH (m1:Movie {title: 'The Matrix'})\n"
75-
+ "MATCH (m2:Movie {title: 'The Matrix Reloaded'})\n"
76-
+ "MATCH (m3:Movie {title: 'The Matrix Revolutions'})\n"
77-
+ "CREATE (m2) - [:IS_SEQUEL_OF] -> (m1)\n"
78-
+ "CREATE (m3) - [:IS_SEQUEL_OF] -> (m2)\n"
79-
+ "RETURN *").consume();
80-
session.run("MATCH (m1:Movie {title: 'The Matrix'})\n"
81-
+ "MATCH (m2:Movie {title: 'The Matrix Reloaded'})\n"
82-
+ "CREATE (p:Person {name: 'Gloria Foster'})\n"
83-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m1)\n"
84-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m2)\n"
85-
+ "RETURN *").consume();
86-
session.run("MATCH (m3:Movie {title: 'The Matrix Revolutions'})\n"
87-
+ "CREATE (p:Person {name: 'Mary Alice'})\n"
88-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m3)\n"
89-
+ "RETURN *").consume();
68+
CypherUtils.loadCypherFromResource("/data/movies.cypher", session);
69+
CypherUtils.loadCypherFromResource("/data/orgstructure.cypher", session);
9070
}
9171
}
9272

9373
interface MovieProjectionWithActorProjection {
9474
String getTitle();
75+
9576
List<ActorProjection> getActors();
9677

9778
interface ActorProjection {
@@ -141,6 +122,38 @@ interface MovieRepository extends Neo4jRepository<Movie, String> {
141122
List<Movie> customPathQueryMoviesFind(@Param("title") String title);
142123
}
143124

125+
@Test // GH-1906
126+
void nestedSelfRelationshipsFromCustomQueryShouldWork(@Autowired Neo4jTemplate template) {
127+
128+
Optional<Partner> optionalPartner = template.findOne(
129+
"MATCH p=(partner:Partner {code: $partnerCode})-[:CHILD_ORGANISATIONS*0..4]->(org:Organisation) \n"
130+
+ "UNWIND nodes(p) as node UNWIND relationships(p) as rel\n"
131+
+ "RETURN partner, collect(distinct node), collect(distinct rel)",
132+
Collections.singletonMap("partnerCode", "partner-one"), Partner.class);
133+
134+
assertThat(optionalPartner).hasValueSatisfying(p -> {
135+
assertThat(p.getName()).isEqualTo("partner one");
136+
137+
assertThat(p.getOrganisations()).hasSize(1);
138+
Organisation org1 = p.getOrganisations().get(0);
139+
140+
assertThat(org1.getCode()).isEqualTo("org-1");
141+
Map<String, Organisation> org1Childs = org1.getOrganisations().stream()
142+
.collect(Collectors.toMap(Organisation::getCode, Function.identity()));
143+
assertThat(org1Childs).hasSize(2);
144+
145+
assertThat(org1Childs).hasEntrySatisfying("org-2", o -> assertThat(o.getOrganisations()).hasSize(1));
146+
assertThat(org1Childs).hasEntrySatisfying("org-6", o -> assertThat(o.getOrganisations()).isEmpty());
147+
148+
Organisation org3 = org1Childs.get("org-2").getOrganisations().get(0);
149+
assertThat(org3.getCode()).isEqualTo("org-3");
150+
151+
Map<String, Organisation> org3Childs = org3.getOrganisations().stream()
152+
.collect(Collectors.toMap(Organisation::getCode, Function.identity()));
153+
assertThat(org3Childs).containsKeys("org-4", "org-5");
154+
});
155+
}
156+
144157
@Test // GH-2117
145158
void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjections(@Autowired MovieRepository movieRepository) {
146159

@@ -164,12 +177,14 @@ void bothCyclicAndNonCyclicRelationshipsAreExcludedFromDTOProjections(@Autowired
164177
}
165178

166179
@Test // GH-2117
167-
void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjectionsWithProjections(@Autowired MovieRepository movieRepository) {
180+
void bothCyclicAndNonCyclicRelationshipsAreExcludedFromProjectionsWithProjections(
181+
@Autowired MovieRepository movieRepository) {
168182

169183
// The movie domain is a good fit for this test
170184
// as the cyclic dependencies is pretty slow to retrieve from Neo4j
171185
// this does OOM in most setups.
172-
MovieProjectionWithActorProjection projection = movieRepository.findProjectionWithProjectionByTitle("The Matrix");
186+
MovieProjectionWithActorProjection projection = movieRepository
187+
.findProjectionWithProjectionByTitle("The Matrix");
173188
assertThat(projection.getTitle()).isNotNull();
174189
assertThat(projection.getActors()).isNotEmpty();
175190
}
@@ -192,15 +207,18 @@ void bothStartAndEndNodeOfPathsMustBeLookedAt(@Autowired Neo4jTemplate template)
192207
@Test // GH-2114
193208
void directionAndTypeLessPathMappingShouldWork(@Autowired Neo4jTemplate template) {
194209

195-
List<Person> people = template.findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), Person.class);
210+
List<Person> people = template
211+
.findAll("MATCH p=(:Person)-[]-(:Person) RETURN p", Collections.emptyMap(), Person.class);
196212
assertThat(people).hasSize(6);
197213
}
198214

199215
@Test // GH-2114
200216
void mappingOfAPathWithOddNumberOfElementsShouldWorkFromStartToEnd(@Autowired Neo4jTemplate template) {
201217

202218
Map<String, Movie> movies = template
203-
.findAll("MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) RETURN p", Collections.emptyMap(), Movie.class)
219+
.findAll(
220+
"MATCH p=shortestPath((:Person {name: 'Mary Alice'})-[*]-(:Person {name: 'Emil Eifrem'})) RETURN p",
221+
Collections.emptyMap(), Movie.class)
204222
.stream().collect(Collectors.toMap(Movie::getTitle, Function.identity()));
205223
assertThat(movies).hasSize(3);
206224

@@ -217,7 +235,9 @@ void mappingOfAPathWithOddNumberOfElementsShouldWorkFromStartToEnd(@Autowired Ne
217235
void mappingOfAPathWithEventNumberOfElementsShouldWorkFromStartToEnd(@Autowired Neo4jTemplate template) {
218236

219237
Map<String, Movie> movies = template
220-
.findAll("MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) RETURN p", Collections.emptyMap(), Movie.class)
238+
.findAll(
239+
"MATCH p=shortestPath((:Movie {title: 'The Matrix Revolutions'})-[*]-(:Person {name: 'Emil Eifrem'})) RETURN p",
240+
Collections.emptyMap(), Movie.class)
221241
.stream().collect(Collectors.toMap(Movie::getTitle, Function.identity()));
222242
assertThat(movies).hasSize(3);
223243

src/test/java/org/springframework/data/neo4j/integration/movies/reactive/ReactiveAdvancedMappingIT.java

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@
1515
*/
1616
package org.springframework.data.neo4j.integration.movies.reactive;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import reactor.core.publisher.Flux;
21+
import reactor.core.publisher.Mono;
22+
import reactor.test.StepVerifier;
23+
24+
import java.io.IOException;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.function.Function;
31+
import java.util.stream.Collectors;
32+
1833
import org.junit.jupiter.api.BeforeAll;
1934
import org.junit.jupiter.api.Tag;
2035
import org.junit.jupiter.api.Test;
@@ -26,6 +41,7 @@
2641
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
2742
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
2843
import org.springframework.data.neo4j.integration.movies.shared.Actor;
44+
import org.springframework.data.neo4j.integration.movies.shared.CypherUtils;
2945
import org.springframework.data.neo4j.integration.movies.shared.Movie;
3046
import org.springframework.data.neo4j.integration.movies.shared.Person;
3147
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
@@ -35,25 +51,10 @@
3551
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
3652
import org.springframework.data.repository.query.Param;
3753
import org.springframework.transaction.annotation.EnableTransactionManagement;
38-
import reactor.core.publisher.Flux;
39-
import reactor.core.publisher.Mono;
40-
import reactor.test.StepVerifier;
41-
42-
import java.io.BufferedReader;
43-
import java.io.IOException;
44-
import java.io.InputStreamReader;
45-
import java.util.ArrayList;
46-
import java.util.Collections;
47-
import java.util.HashMap;
48-
import java.util.List;
49-
import java.util.Map;
50-
import java.util.function.Function;
51-
import java.util.stream.Collectors;
52-
53-
import static org.assertj.core.api.Assertions.assertThat;
5454

5555
/**
5656
* @author Gerrit Meier
57+
* @author Michael J. Simons
5758
*/
5859
@Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT)
5960
@Neo4jIntegrationTest
@@ -64,32 +65,9 @@ class ReactiveAdvancedMappingIT {
6465
@BeforeAll
6566
static void setupData(@Autowired Driver driver) throws IOException {
6667

67-
try (BufferedReader moviesReader = new BufferedReader(
68-
new InputStreamReader(ReactiveAdvancedMappingIT.class.getResourceAsStream("/data/movies.cypher")));
69-
Session session = driver.session()) {
68+
try (Session session = driver.session()) {
7069
session.run("MATCH (n) DETACH DELETE n").consume();
71-
String moviesCypher = moviesReader.lines().collect(Collectors.joining(" "));
72-
session.run(moviesCypher).consume();
73-
session.run("MATCH (l1:Person {name: 'Lilly Wachowski'})\n"
74-
+ "MATCH (l2:Person {name: 'Lana Wachowski'})\n"
75-
+ "CREATE (l1) - [s:IS_SIBLING_OF] -> (l2)\n"
76-
+ "RETURN *").consume();
77-
session.run("MATCH (m1:Movie {title: 'The Matrix'})\n"
78-
+ "MATCH (m2:Movie {title: 'The Matrix Reloaded'})\n"
79-
+ "MATCH (m3:Movie {title: 'The Matrix Revolutions'})\n"
80-
+ "CREATE (m2) - [:IS_SEQUEL_OF] -> (m1)\n"
81-
+ "CREATE (m3) - [:IS_SEQUEL_OF] -> (m2)\n"
82-
+ "RETURN *").consume();
83-
session.run("MATCH (m1:Movie {title: 'The Matrix'})\n"
84-
+ "MATCH (m2:Movie {title: 'The Matrix Reloaded'})\n"
85-
+ "CREATE (p:Person {name: 'Gloria Foster'})\n"
86-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m1)\n"
87-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m2)\n"
88-
+ "RETURN *").consume();
89-
session.run("MATCH (m3:Movie {title: 'The Matrix Revolutions'})\n"
90-
+ "CREATE (p:Person {name: 'Mary Alice'})\n"
91-
+ "CREATE (p) -[:ACTED_IN {roles: ['The Oracle']}] -> (m3)\n"
92-
+ "RETURN *").consume();
70+
CypherUtils.loadCypherFromResource("/data/movies.cypher", session);
9371
}
9472
}
9573

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.integration.movies.shared;
17+
18+
import java.io.BufferedReader;
19+
import java.io.IOException;
20+
import java.io.InputStreamReader;
21+
import java.util.stream.Collectors;
22+
23+
import org.neo4j.driver.Session;
24+
25+
/**
26+
* @author Michael J. Simons
27+
*/
28+
public final class CypherUtils {
29+
30+
public static void loadCypherFromResource(String resource, Session session) throws IOException {
31+
try (BufferedReader moviesReader = new BufferedReader(
32+
new InputStreamReader(CypherUtils.class.getResourceAsStream(resource)))) {
33+
for (String statement : moviesReader.lines().collect(Collectors.joining(" ")).split(";")) {
34+
session.run(statement).consume();
35+
}
36+
}
37+
}
38+
39+
private CypherUtils() {
40+
}
41+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.integration.movies.shared;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
22+
import org.springframework.data.neo4j.core.schema.Id;
23+
import org.springframework.data.neo4j.core.schema.Node;
24+
import org.springframework.data.neo4j.core.schema.Relationship;
25+
26+
/**
27+
* @soundtrack Guns n' Roses - Appetite For Destruction
28+
*/
29+
@Node
30+
public class Organisation {
31+
32+
@Id
33+
@GeneratedValue
34+
private Long id;
35+
private final String partnerCode;
36+
private final String code;
37+
private final String name;
38+
private final String type;
39+
40+
@Relationship(type = "CHILD_ORGANISATIONS")
41+
private final List<Organisation> organisations;
42+
43+
public Organisation(String partnerCode, String code, String name, String type,
44+
List<Organisation> organisations) {
45+
this.partnerCode = partnerCode;
46+
this.code = code;
47+
this.name = name;
48+
this.type = type;
49+
this.organisations = organisations;
50+
}
51+
52+
public Organisation withId(Long newId) {
53+
if (this.id == newId) {
54+
return this;
55+
}
56+
Organisation o = new Organisation(this.partnerCode, this.code, this.name, this.type, this.organisations);
57+
o.id = newId;
58+
return o;
59+
}
60+
61+
public Long getId() {
62+
return id;
63+
}
64+
65+
public String getPartnerCode() {
66+
return partnerCode;
67+
}
68+
69+
public String getCode() {
70+
return code;
71+
}
72+
73+
public String getName() {
74+
return name;
75+
}
76+
77+
public String getType() {
78+
return type;
79+
}
80+
81+
public List<Organisation> getOrganisations() {
82+
return organisations == null ? Collections.emptyList() : Collections.unmodifiableList(organisations);
83+
}
84+
85+
@Override public String toString() {
86+
return "Organisation{" +
87+
"id=" + id +
88+
", partnerCode='" + partnerCode + '\'' +
89+
", code='" + code + '\'' +
90+
", name='" + name + '\'' +
91+
", type='" + type + '\'' +
92+
", organisations=" + organisations +
93+
'}';
94+
}
95+
}

0 commit comments

Comments
 (0)