Skip to content

Commit cb61f79

Browse files
GH-2210 - Improve documentation for hydrating collections based on queries returning paths.
This closes #2210.
1 parent b123dbb commit cb61f79

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

src/main/asciidoc/appendix/custom-queries.adoc

+46
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,52 @@ NOTE: Deciding if you want to go with client-side or database-side reduction dep
125125
All the paths needs to get created in the database's memory first when the `reduce` function is used.
126126
On the other hand a large amount of data that needs to get merged on the client-side results in a higher memory usage there.
127127

128+
[[custom-query.paths]]
129+
== Using paths to populate and return a list of entities
130+
131+
Given are a graph that looks like this:
132+
133+
[[custom-query.paths.g]]
134+
.graph with outgoing relationships
135+
image::custom-query.paths.png[]
136+
137+
and a domain model as shown in the <<custom-query.paths.dm,mapping>> (Constructors and accessors have been omitted for brevity):
138+
139+
[[custom-query.paths.dm]]
140+
[source,java,indent=0,tabsize=4]
141+
.Domain model for a <<custom-query.paths.g>>.
142+
----
143+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/GH2210IT.java[tag=custom-query.paths.dm]
144+
----
145+
146+
As you see, the relationships are only outgoing. Generated finder methods (including `findById`) will always try to match
147+
a root node to be mapped. From there on onwards, all related objects will be mapped. In queries that should return only one object,
148+
that root object is returned. In queries that return many objects, all matching objects are returned. Out- and incoming relationships
149+
from those objects returned are of course populated.
150+
151+
Assume the following Cypher query:
152+
153+
[source,cypher]
154+
----
155+
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
156+
RETURN leaf, collect(nodes(p)), collect(relationships(p))
157+
----
158+
159+
It follows the recommendation from <<custom-queries.for-relationships.one.record>> and it works great for the leaf node
160+
you want to match here. However: That is only the case in all scenarios that return 0 or 1 mapped objects.
161+
While that query will populate all relationships like before, it won't return all 4 objects.
162+
163+
This can be changed by returning the whole path:
164+
165+
[source,cypher]
166+
----
167+
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
168+
RETURN p
169+
----
170+
171+
Here we do want to use the fact that the path `p` actually returns 3 rows with paths to all 4 nodes. All 4 nodes will be
172+
populated, linked together and returned.
173+
128174
[[custom-queries.parameters]]
129175
== Parameters in custom queries
130176

src/main/asciidoc/faq/faq.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,8 @@ The query returns the path plus all relationships and related nodes collected so
550550

551551
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
552552

553+
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
554+
553555
[[faq.spring-boot.sdn]]
554556
== Do I need Spring Boot to use Spring Data Neo4j?
555557

14.5 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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.issues.gh2210;
17+
18+
import org.junit.jupiter.api.BeforeAll;
19+
import org.junit.jupiter.api.Test;
20+
import org.neo4j.driver.Driver;
21+
import org.neo4j.driver.Record;
22+
import org.neo4j.driver.Transaction;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
27+
import org.springframework.data.neo4j.core.Neo4jTemplate;
28+
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
29+
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
30+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
31+
import org.springframework.data.neo4j.core.schema.Id;
32+
import org.springframework.data.neo4j.core.schema.Node;
33+
import org.springframework.data.neo4j.core.schema.Relationship;
34+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
35+
import org.springframework.data.neo4j.core.schema.TargetNode;
36+
import org.springframework.data.neo4j.test.Neo4jExtension;
37+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
38+
import org.springframework.transaction.annotation.EnableTransactionManagement;
39+
40+
import java.util.Arrays;
41+
import java.util.Collections;
42+
import java.util.HashMap;
43+
import java.util.HashSet;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.Optional;
47+
import java.util.Set;
48+
49+
import static org.assertj.core.api.Assertions.assertThat;
50+
51+
/**
52+
* @author Michael J. Simons
53+
*/
54+
@Neo4jIntegrationTest
55+
class GH2210IT {
56+
57+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
58+
59+
static final Long numberA = 1L;
60+
static final Long numberB = 2L;
61+
static final Long numberC = 3L;
62+
static final Long numberD = 4L;
63+
64+
@BeforeAll
65+
protected static void setupData() {
66+
try (Transaction transaction = neo4jConnectionSupport.getDriver().session().beginTransaction()) {
67+
transaction.run("MATCH (n) detach delete n");
68+
Map<String, Object> params = new HashMap<>();
69+
params.put("numberA", numberA);
70+
params.put("numberB", numberB);
71+
params.put("numberC", numberC);
72+
params.put("numberD", numberD);
73+
Record r = transaction.run("create (a:SomeEntity {number: $numberA, name: \"A\"})\n"
74+
+ "create (b:SomeEntity {number: $numberB, name: \"B\"})\n"
75+
+ "create (c:SomeEntity {number: $numberC, name: \"C\"})\n"
76+
+ "create (d:SomeEntity {number: $numberD, name: \"D\"})\n"
77+
+ "create (a) -[:SOME_RELATION_TO {someData: \"d1\"}] -> (b)\n"
78+
+ "create (b) <-[:SOME_RELATION_TO {someData: \"d2\"}] - (c)\n"
79+
+ "create (c) <-[:SOME_RELATION_TO {someData: \"d3\"}] - (d)\n"
80+
+ "return * ", params).single();
81+
transaction.commit();
82+
}
83+
}
84+
85+
@Test // GH-2210
86+
void standardFinderShouldWork(@Autowired Neo4jTemplate template) {
87+
88+
assertA(template.findById(numberA, SomeEntity.class));
89+
90+
assertB(template.findById(numberB, SomeEntity.class));
91+
92+
assertD(template.findById(numberD, SomeEntity.class));
93+
}
94+
95+
@Test // GH-2210
96+
void pathsBasedQueryShouldWork(@Autowired Neo4jTemplate template) {
97+
98+
String query = "MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity) RETURN leaf, collect(nodes(p)), collect(relationships(p))";
99+
assertA(template.findOne(query, Collections.singletonMap("a", numberA), SomeEntity.class));
100+
101+
assertB(template.findOne(query, Collections.singletonMap("a", numberB), SomeEntity.class));
102+
103+
assertD(template.findOne(query, Collections.singletonMap("a", numberD), SomeEntity.class));
104+
}
105+
106+
@Test // GH-2210
107+
void aPathReturnedShouldPopulateAllNodes(@Autowired Neo4jTemplate template) {
108+
109+
String query = "MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity) RETURN p";
110+
assertAll(template.findAll(query, Collections.singletonMap("a", numberA), SomeEntity.class));
111+
}
112+
113+
@Test // GH-2210
114+
void standardFindAllShouldWork(@Autowired Neo4jTemplate template) {
115+
116+
assertAll(template.findAll(SomeEntity.class));
117+
}
118+
119+
void assertAll(List<SomeEntity> entities) {
120+
121+
assertThat(entities).hasSize(4);
122+
assertThat(entities).allSatisfy(v -> {
123+
switch (v.getName()) {
124+
case "A":
125+
assertA(Optional.of(v));
126+
break;
127+
case "B":
128+
assertB(Optional.of(v));
129+
break;
130+
case "D":
131+
assertD(Optional.of(v));
132+
break;
133+
}
134+
});
135+
}
136+
137+
void assertA(Optional<SomeEntity> a) {
138+
139+
assertThat(a).hasValueSatisfying(s -> {
140+
assertThat(s.getName()).isEqualTo("A");
141+
assertThat(s.getSomeRelationsOut())
142+
.hasSize(1)
143+
.first().satisfies(b -> {
144+
assertThat(b.getSomeData()).isEqualTo("d1");
145+
assertThat(b.getTargetPerson().getName()).isEqualTo("B");
146+
assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty();
147+
});
148+
});
149+
}
150+
151+
void assertD(Optional<SomeEntity> d) {
152+
153+
assertThat(d).hasValueSatisfying(s -> {
154+
assertThat(s.getName()).isEqualTo("D");
155+
assertThat(s.getSomeRelationsOut())
156+
.hasSize(1)
157+
.first().satisfies(c -> {
158+
assertThat(c.getSomeData()).isEqualTo("d3");
159+
assertThat(c.getTargetPerson().getName()).isEqualTo("C");
160+
assertThat(c.getTargetPerson().getSomeRelationsOut())
161+
.hasSize(1)
162+
.first().satisfies(b -> {
163+
assertThat(b.getSomeData()).isEqualTo("d2");
164+
assertThat(b.getTargetPerson().getName()).isEqualTo("B");
165+
assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty();
166+
});
167+
});
168+
});
169+
}
170+
171+
void assertB(Optional<SomeEntity> b) {
172+
173+
assertThat(b).hasValueSatisfying(s -> {
174+
assertThat(s.getName()).isEqualTo("B");
175+
assertThat(s.getSomeRelationsOut()).isEmpty();
176+
});
177+
}
178+
179+
// tag::custom-query.paths.dm[]
180+
@Node
181+
static class SomeEntity {
182+
183+
@Id
184+
private final Long number;
185+
186+
private String name;
187+
188+
@Relationship(type = "SOME_RELATION_TO", direction = Relationship.Direction.OUTGOING)
189+
private Set<SomeRelation> someRelationsOut = new HashSet<>();
190+
// end::custom-query.paths.dm[]
191+
192+
public Long getNumber() {
193+
return number;
194+
}
195+
196+
public String getName() {
197+
return name;
198+
}
199+
200+
public Set<SomeRelation> getSomeRelationsOut() {
201+
return someRelationsOut;
202+
}
203+
204+
SomeEntity(Long number) {
205+
this.number = number;
206+
}
207+
// tag::custom-query.paths.dm[]
208+
}
209+
210+
@RelationshipProperties
211+
static class SomeRelation {
212+
213+
@Id @GeneratedValue
214+
private Long id;
215+
216+
private String someData;
217+
218+
@TargetNode
219+
private SomeEntity targetPerson;
220+
// end::custom-query.paths.dm[]
221+
222+
public Long getId() {
223+
return id;
224+
}
225+
226+
public String getSomeData() {
227+
return someData;
228+
}
229+
230+
public SomeEntity getTargetPerson() {
231+
return targetPerson;
232+
}
233+
// tag::custom-query.paths.dm[]
234+
}
235+
// end::custom-query.paths.dm[]
236+
237+
@Configuration
238+
@EnableTransactionManagement
239+
static class Config extends AbstractNeo4jConfig {
240+
241+
@Bean
242+
public Driver driver() {
243+
244+
return neo4jConnectionSupport.getDriver();
245+
}
246+
247+
@Override
248+
public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException {
249+
250+
Neo4jMappingContext ctx = new Neo4jMappingContext(neo4JConversions);
251+
ctx.setInitialEntitySet(new HashSet<>(Arrays.asList(SomeEntity.class, SomeRelation.class)));
252+
return ctx;
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)