GH-2210 - Improve documentation for hydrating collections based on queries returning paths.
This closes #2210.
This commit is contained in:
@@ -125,6 +125,52 @@ NOTE: Deciding if you want to go with client-side or database-side reduction dep
|
||||
All the paths needs to get created in the database's memory first when the `reduce` function is used.
|
||||
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.
|
||||
|
||||
[[custom-query.paths]]
|
||||
== Using paths to populate and return a list of entities
|
||||
|
||||
Given are a graph that looks like this:
|
||||
|
||||
[[custom-query.paths.g]]
|
||||
.graph with outgoing relationships
|
||||
image::custom-query.paths.png[]
|
||||
|
||||
and a domain model as shown in the <<custom-query.paths.dm,mapping>> (Constructors and accessors have been omitted for brevity):
|
||||
|
||||
[[custom-query.paths.dm]]
|
||||
[source,java,indent=0,tabsize=4]
|
||||
.Domain model for a <<custom-query.paths.g>>.
|
||||
----
|
||||
include::../../../../src/test/java/org/springframework/data/neo4j/integration/issues/gh2210/GH2210IT.java[tag=custom-query.paths.dm]
|
||||
----
|
||||
|
||||
As you see, the relationships are only outgoing. Generated finder methods (including `findById`) will always try to match
|
||||
a root node to be mapped. From there on onwards, all related objects will be mapped. In queries that should return only one object,
|
||||
that root object is returned. In queries that return many objects, all matching objects are returned. Out- and incoming relationships
|
||||
from those objects returned are of course populated.
|
||||
|
||||
Assume the following Cypher query:
|
||||
|
||||
[source,cypher]
|
||||
----
|
||||
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
|
||||
RETURN leaf, collect(nodes(p)), collect(relationships(p))
|
||||
----
|
||||
|
||||
It follows the recommendation from <<custom-queries.for-relationships.one.record>> and it works great for the leaf node
|
||||
you want to match here. However: That is only the case in all scenarios that return 0 or 1 mapped objects.
|
||||
While that query will populate all relationships like before, it won't return all 4 objects.
|
||||
|
||||
This can be changed by returning the whole path:
|
||||
|
||||
[source,cypher]
|
||||
----
|
||||
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
|
||||
RETURN p
|
||||
----
|
||||
|
||||
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
|
||||
populated, linked together and returned.
|
||||
|
||||
[[custom-queries.parameters]]
|
||||
== Parameters in custom queries
|
||||
|
||||
|
||||
@@ -550,6 +550,8 @@ The query returns the path plus all relationships and related nodes collected so
|
||||
|
||||
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
|
||||
|
||||
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
|
||||
|
||||
[[faq.spring-boot.sdn]]
|
||||
== Do I need Spring Boot to use Spring Data Neo4j?
|
||||
|
||||
|
||||
BIN
src/main/asciidoc/img/custom-query.paths.png
Normal file
BIN
src/main/asciidoc/img/custom-query.paths.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2011-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2210;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.schema.GeneratedValue;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.core.schema.Relationship;
|
||||
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
|
||||
import org.springframework.data.neo4j.core.schema.TargetNode;
|
||||
import org.springframework.data.neo4j.test.Neo4jExtension;
|
||||
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Neo4jIntegrationTest
|
||||
class GH2210IT {
|
||||
|
||||
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
|
||||
|
||||
static final Long numberA = 1L;
|
||||
static final Long numberB = 2L;
|
||||
static final Long numberC = 3L;
|
||||
static final Long numberD = 4L;
|
||||
|
||||
@BeforeAll
|
||||
protected static void setupData() {
|
||||
try (Transaction transaction = neo4jConnectionSupport.getDriver().session().beginTransaction()) {
|
||||
transaction.run("MATCH (n) detach delete n");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("numberA", numberA);
|
||||
params.put("numberB", numberB);
|
||||
params.put("numberC", numberC);
|
||||
params.put("numberD", numberD);
|
||||
Record r = transaction.run("create (a:SomeEntity {number: $numberA, name: \"A\"})\n"
|
||||
+ "create (b:SomeEntity {number: $numberB, name: \"B\"})\n"
|
||||
+ "create (c:SomeEntity {number: $numberC, name: \"C\"})\n"
|
||||
+ "create (d:SomeEntity {number: $numberD, name: \"D\"})\n"
|
||||
+ "create (a) -[:SOME_RELATION_TO {someData: \"d1\"}] -> (b)\n"
|
||||
+ "create (b) <-[:SOME_RELATION_TO {someData: \"d2\"}] - (c)\n"
|
||||
+ "create (c) <-[:SOME_RELATION_TO {someData: \"d3\"}] - (d)\n"
|
||||
+ "return * ", params).single();
|
||||
transaction.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test // GH-2210
|
||||
void standardFinderShouldWork(@Autowired Neo4jTemplate template) {
|
||||
|
||||
assertA(template.findById(numberA, SomeEntity.class));
|
||||
|
||||
assertB(template.findById(numberB, SomeEntity.class));
|
||||
|
||||
assertD(template.findById(numberD, SomeEntity.class));
|
||||
}
|
||||
|
||||
@Test // GH-2210
|
||||
void pathsBasedQueryShouldWork(@Autowired Neo4jTemplate template) {
|
||||
|
||||
String query = "MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity) RETURN leaf, collect(nodes(p)), collect(relationships(p))";
|
||||
assertA(template.findOne(query, Collections.singletonMap("a", numberA), SomeEntity.class));
|
||||
|
||||
assertB(template.findOne(query, Collections.singletonMap("a", numberB), SomeEntity.class));
|
||||
|
||||
assertD(template.findOne(query, Collections.singletonMap("a", numberD), SomeEntity.class));
|
||||
}
|
||||
|
||||
@Test // GH-2210
|
||||
void aPathReturnedShouldPopulateAllNodes(@Autowired Neo4jTemplate template) {
|
||||
|
||||
String query = "MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity) RETURN p";
|
||||
assertAll(template.findAll(query, Collections.singletonMap("a", numberA), SomeEntity.class));
|
||||
}
|
||||
|
||||
@Test // GH-2210
|
||||
void standardFindAllShouldWork(@Autowired Neo4jTemplate template) {
|
||||
|
||||
assertAll(template.findAll(SomeEntity.class));
|
||||
}
|
||||
|
||||
void assertAll(List<SomeEntity> entities) {
|
||||
|
||||
assertThat(entities).hasSize(4);
|
||||
assertThat(entities).allSatisfy(v -> {
|
||||
switch (v.getName()) {
|
||||
case "A":
|
||||
assertA(Optional.of(v));
|
||||
break;
|
||||
case "B":
|
||||
assertB(Optional.of(v));
|
||||
break;
|
||||
case "D":
|
||||
assertD(Optional.of(v));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void assertA(Optional<SomeEntity> a) {
|
||||
|
||||
assertThat(a).hasValueSatisfying(s -> {
|
||||
assertThat(s.getName()).isEqualTo("A");
|
||||
assertThat(s.getSomeRelationsOut())
|
||||
.hasSize(1)
|
||||
.first().satisfies(b -> {
|
||||
assertThat(b.getSomeData()).isEqualTo("d1");
|
||||
assertThat(b.getTargetPerson().getName()).isEqualTo("B");
|
||||
assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void assertD(Optional<SomeEntity> d) {
|
||||
|
||||
assertThat(d).hasValueSatisfying(s -> {
|
||||
assertThat(s.getName()).isEqualTo("D");
|
||||
assertThat(s.getSomeRelationsOut())
|
||||
.hasSize(1)
|
||||
.first().satisfies(c -> {
|
||||
assertThat(c.getSomeData()).isEqualTo("d3");
|
||||
assertThat(c.getTargetPerson().getName()).isEqualTo("C");
|
||||
assertThat(c.getTargetPerson().getSomeRelationsOut())
|
||||
.hasSize(1)
|
||||
.first().satisfies(b -> {
|
||||
assertThat(b.getSomeData()).isEqualTo("d2");
|
||||
assertThat(b.getTargetPerson().getName()).isEqualTo("B");
|
||||
assertThat(b.getTargetPerson().getSomeRelationsOut()).isEmpty();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void assertB(Optional<SomeEntity> b) {
|
||||
|
||||
assertThat(b).hasValueSatisfying(s -> {
|
||||
assertThat(s.getName()).isEqualTo("B");
|
||||
assertThat(s.getSomeRelationsOut()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
// tag::custom-query.paths.dm[]
|
||||
@Node
|
||||
static class SomeEntity {
|
||||
|
||||
@Id
|
||||
private final Long number;
|
||||
|
||||
private String name;
|
||||
|
||||
@Relationship(type = "SOME_RELATION_TO", direction = Relationship.Direction.OUTGOING)
|
||||
private Set<SomeRelation> someRelationsOut = new HashSet<>();
|
||||
// end::custom-query.paths.dm[]
|
||||
|
||||
public Long getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Set<SomeRelation> getSomeRelationsOut() {
|
||||
return someRelationsOut;
|
||||
}
|
||||
|
||||
SomeEntity(Long number) {
|
||||
this.number = number;
|
||||
}
|
||||
// tag::custom-query.paths.dm[]
|
||||
}
|
||||
|
||||
@RelationshipProperties
|
||||
static class SomeRelation {
|
||||
|
||||
@Id @GeneratedValue
|
||||
private Long id;
|
||||
|
||||
private String someData;
|
||||
|
||||
@TargetNode
|
||||
private SomeEntity targetPerson;
|
||||
// end::custom-query.paths.dm[]
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getSomeData() {
|
||||
return someData;
|
||||
}
|
||||
|
||||
public SomeEntity getTargetPerson() {
|
||||
return targetPerson;
|
||||
}
|
||||
// tag::custom-query.paths.dm[]
|
||||
}
|
||||
// end::custom-query.paths.dm[]
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
static class Config extends AbstractNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
|
||||
return neo4jConnectionSupport.getDriver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException {
|
||||
|
||||
Neo4jMappingContext ctx = new Neo4jMappingContext(neo4JConversions);
|
||||
ctx.setInitialEntitySet(new HashSet<>(Arrays.asList(SomeEntity.class, SomeRelation.class)));
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user