GH-2210 - Improve documentation for hydrating collections based on queries returning paths.

This closes #2210.
This commit is contained in:
Michael Simons
2021-04-19 15:13:25 +02:00
parent a549ed7404
commit c75df632ca
4 changed files with 303 additions and 0 deletions

View File

@@ -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

View File

@@ -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?

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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;
}
}
}