Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
BenBohannon opened this issue Apr 1, 2021 · 6 comments
Assignees
Labels
type: documentation A documentation update

Comments

@BenBohannon
Copy link

BenBohannon commented Apr 1, 2021

On 6.0.7, I'm trying to use a custom query like this:

MATCH p = (leaf:PERSON {number: $a})-[:SOME_RELATION_TO*]-(:PERSON) 
RETURN leaf, collect(nodes(p)), collect(relationships(p))

to get all paths from a single PERSON node to all other PERSON nodes connected by SOME_RELATION_TO regardless of direction or depth, and I also need the relationship properties between each node.

Entities:

@Node("PERSON")
public class Person {
    @Id
    private final Long number;

    @Property("name")
    private String name;

    @Relationship(type = "SOME_RELATION_TO", direction = OUTGOING)
    private Set<SomeRelation> someRelationsOut = new HashSet<>();
}
@RelationshipProperties
public class SomeRelation {
    @Id @GeneratedValue private Long id;

    @Property private double someData;

    @TargetNode private Person targetPerson;
}
@Query("MATCH p = (leaf:PERSON {number: $a})-[:SOME_RELATION_TO*]-(:PERSON) RETURN leaf, collect(nodes(p)), collect(relationships(p))")
List<Person> getRelated(Long a);

My problem is that performing this query from a leaf node (only incoming directions) yields a list containing only the leaf node without any other relationships (since it technically has none outgoing?). The query only returns other nodes/relationships when performed from a node with outgoing directions.

My graph is acylic and only composed of a single node and relation type.
E.g. Person A -> Person B <- Person C <- Person D
If the query is performed from Person A, then I only get Persons A and B. If performed from Person B, then I only get Person B.

Should my query be different? Or is this a limitation of the object mapping or bi-directional relationships?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 1, 2021
@michael-simons
Copy link
Collaborator

Did this work with 6.0.6?

@michael-simons
Copy link
Collaborator

Oh, I guess I see what you want to achieve…
Our relationships are directed.

Here's a test for the standard finders with your model. Does this clear things up?

/*
 * 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 static org.assertj.core.api.Assertions.assertThat;

import java.util.Optional;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
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.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Michael J. Simons
 */
@Neo4jIntegrationTest
class GH2210IT {

	protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;

	@BeforeAll
	protected static void setupData() {
		try (Transaction transaction = neo4jConnectionSupport.getDriver().session().beginTransaction()) {
			transaction.run("MATCH (n) detach delete n");
			transaction.run("create (a:SomeEntity {number:1, name: \"A\"})\n"
							+ "create (b:SomeEntity {number:2, name: \"B\"})\n"
							+ "create (c:SomeEntity {number:3, name: \"C\"})\n"
							+ "create (d:SomeEntity {number:4, 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 * ");
			transaction.commit();
		}
	}
	
	@Test // GH-2210
	void standardFinderShouldWork(@Autowired SomeRepository someRepository) {

		Optional<SomeEntity> a = someRepository.findById(1L);
		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();
			});
		});

		Optional<SomeEntity> d = someRepository.findById(4L);
		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();
				});
			});
		});
	}

	interface SomeRepository extends Neo4jRepository<SomeEntity, Long> {
	}

	@Configuration
	@EnableTransactionManagement
	@EnableNeo4jRepositories(considerNestedRepositories = true)
	static class Config extends AbstractNeo4jConfig {

		@Bean
		public Driver driver() {

			return neo4jConnectionSupport.getDriver();
		}
	}
}

@BenBohannon
Copy link
Author

It didn't work with 6.0.6 either.

The test uses findById, and it has the behavior I would expect when querying for a single node: the Java object only stores outgoing relationships, so we lose/don't see any incoming ones. (It would be nice to get both incoming and outgoing relations, but that's another discussion).

When querying for paths, I would expect the returned List of Nodes to be populated with all the nodes found in each path and hydrated with the distinct relationships of each path (since Neo4j returns all that data in the query). Instead, it seems to build the List starting from the "root" node, which hits a dead end if all the relationships are incoming (and your Java object only stores outgoing).

@BenBohannon
Copy link
Author

Well, after thinking some more a "list of paths" doesn't make sense, since a list can only contain one path. Maybe a list of lists with one path each would make more sense.

Ultimately, my problem seems to come down to a lack of having both incoming and outgoing relations on a given Node entity. Otherwise I could follow either direction to explore the returned paths.

@michael-simons
Copy link
Collaborator

michael-simons commented Apr 2, 2021 via email

@michael-simons michael-simons self-assigned this Apr 9, 2021
@michael-simons
Copy link
Collaborator

michael-simons commented Apr 19, 2021

Your use case will work when you write your query slightly different and return the path:

String query = "MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity) RETURN p";

I am gonna update that part of the docs for this use case:

https://docs.spring.io/spring-data/neo4j/docs/6.1.0/reference/html/#faq.path-mapping

@michael-simons michael-simons changed the title Map paths starting from leaf node Improve documentation for returning collections based on queries returning paths. Apr 19, 2021
@michael-simons michael-simons changed the title Improve documentation for returning collections based on queries returning paths. Improve documentation for hydrating collections based on queries returning paths. Apr 19, 2021
@michael-simons michael-simons added type: documentation A documentation update and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 19, 2021
michael-simons added a commit that referenced this issue Apr 19, 2021
michael-simons added a commit that referenced this issue Apr 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

3 participants