How to get spring neo4j cypher custom query to pop

2019-08-01 17:07发布

问题:

Built-in queries to Spring Data Neo4j (SDN) return objects populated with depth 1 by default. This means that "children" (related nodes) of an object returned by a query are populated. That's good - there are actual objects on the end of references from objects returned by these queries.

Custom queries are depth 0 by default. This is a hassle.

In this answer, it is described how to get springboot neo4j to populate a related element to the target of a custom query - to achieve an extra one level of depth of results from the query.

I am having trouble with this method when the related elements are in a list:

@NodeEntity
public class BoardPosition {

    @Relationship(type="PARENT", direction = Relationship.INCOMING)
    public List<BoardPosition> children;

I have a query returning a target BoardPosition and I need it's children to be populated.

@Query("MATCH (target:BoardPosition) <-[c:PARENT]- (child:BoardPosition) 
       WHERE target.play={Play} 
       RETURN target, c, child")
BoardPosition findActiveByPlay(@Param("Play") String play);

The problem is that the query appears to return one separate result for each child, and those results aren't being used to populate the array of children in the target.

Instead of Spring Neo collating the children into the array on the target, I get "only 1 result expected" error - as if the query is returning multiple results each with one child, rather than one result with the children in it.

org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected at most 1

How can I have a custom query to populate that target's children list?

(Note that the built-in findByPlay(play) does what I want - the built-in queries have a depth of 1 rather than 0, and it returns a target with populated children - but of course I need to make the query a bit more sophisticated than just "by Play"... that's why I need to solve this)


Versions:

org.springframework.data:spring-data-neo4j:5.1.3.RELEASE

neo4j 3.5.0

回答1:

=== Edit ======

Your problem arises because you have self-relationship (relationship between nodes of the same label)

This is how Spring treat your query for single node:

org.springframework.data.neo4j.repository.query.GraphQueryExecution

@Override
public Object execute(Query query, Class<?> type) {
    Iterable<?> result;
    ....
    Object ret = iterator.next();
    if (iterator.hasNext()) {
        throw new IncorrectResultSizeDataAccessException("Incorrect result size: expected at most 1", 1);
    }
    return ret;
}

Spring passes your node class type Class<?> type to neo4j-ogm and have your data read back.

You know, neo4j server will returns multiple rows for your query, one for each matching path:

A <- PARENT - B
A <- PARENT - C
A <- PARENT - D

If your nodes are of different labels, i.e. of different class type then the ogm only return single node correspond to your query return type, no problem.

But your nodes are of the same labels, i.e. same class type => Neo4j OGM cannot distinguish which is the returned node -> All nodes A, B, C, D returned -> Exception

Regard this issue, I think you should file a bug report now.

For workaround, you can can change the query to return only the distinct target.your_identity_property (identity_property is 'primary key' of the node, which uniquely identify your node)

Then have your application call load with the that identity property:

public interface BoardRepository extends CrudRepository<BoardPos, Long> {

    @Query("MATCH (target:B) <-[c:PARENT]- (child:B) WHERE target.play={Play} RETURN DISTINCT target.your_identity_property")
    Long findActiveByPlay(@Param("Play") String play);

    BoardPos findByYourIdentityProperty(xxxx);
} 

=== OLD ======

Spring docs says that (highlighted by me):

Custom queries do not support a custom depth. Additionally, @Query does not support mapping a path to domain entities, as such, a path should not be returned from a Cypher query. Instead, return nodes and relationships to have them mapped to domain entities.

So clearly your use-case (populate children nodes by custom query) is supported. Spring framework already maps the results into a single node. (Indeed, my setup on local turnouts that the operation is working properly)

So your exception may be caused by several issues:

  1. You have more than one target:BoardPosition with target.play={play}. So the exception refers to more than one target:BoardPosition instead of one BoardPosition with multiple child result

  2. You have incorrect entity mapping. Do you have your mapping field annotated with @Relationship with correct direction attribute? You might post your entity here.

Here is my local setup:

@NodeEntity(label = "C")
@Data
public class Child {

    @Id
    @GeneratedValue
    private long id;
    private String name;

    @Relationship(type = "PARENT", direction = "INCOMING")
    private List<Parent> parents;
}

public interface ChildRepository extends CrudRepository<Child, Long> {

    @Query("MATCH (target:C) <-[p:PARENT]- (child:P) " 
        + "WHERE target.name={name} " 
        + "RETURN target, p, child")
    Child findByName(@Param("name") String name);
}

(:C) <-[:PARENT] - (:P)


回答2:

Consider the alternative query

MATCH (target:BoardPosition {play:{Play}})
RETURN target, [ (target)<-[c:PARENT]-(child:BoardPosition) | [c, child] ]

which is using list comprehension to return not only the target but also its relations and related nodes of label BoardPosition within one result row. This ensures that the result will be a single row (as long as your attribute play is unique).

I didn't try it with your example but in my application this approach is working fine. Neo4j OGM hydrates the objects as expected. It is important to include the related nodes as well as the relations pointing to the nodes.

If you enable neo4j OGM logs, you can see that the build-in queries with depth 1 use the same approach.