0

In order to model a tree/hierarchy (where the parent-child relationship can be traversed both ways) with Spring Data Neo4j 4.1, I wrote the following entity class

@NodeEntity(label = "node")
public class Node {

    @GraphId
    @SuppressWarnings("unused")
    private Long graphId;

    private String name;

    @Relationship(type = "PARENT", direction = Relationship.OUTGOING)
    private Node parent;

    @Relationship(type = "PARENT", direction = Relationship.INCOMING)
    private Iterable<Node> children;

    @SuppressWarnings("unused")
    protected Node() {
        // For SDN.
    }

    public Node(String name, Node parent) {
        this.name = Objects.requireNonNull(name);
        this.parent = parent;
    }

    public String getName() {
        return name;
    }

    public Node getParent() {
        return parent;
    }
}

The problem is that, apparently, the presence of the children field screws up the PARENT relation such that there can be only one such incoming relation for a node. That is, as demonstrated by the following test case, a node cannot have more than one child - "conflicting" relations are automatically deleted:

@RunWith(SpringRunner.class)
@SpringBootTest(
        classes = GraphDomainTestConfig.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE
)
@SuppressWarnings("SpringJavaAutowiredMembersInspection")
public class NodeTest {

    @Autowired
    private NodeRepository repository;

    @Test
    public void test() {
        // Breakpoint 0

        Node A = new Node("A", null);

        A = repository.save(A);
        // Breakpoint 1

        Node B = new Node("B", A);
        Node C = new Node("C", A);

        B = repository.save(B);
        // Breakpoint 2

        C = repository.save(C);
        // Breakpoint 3

        A = repository.findByName("A");
        B = repository.findByName("B");
        C = repository.findByName("C");
        // Breakpoint 4

        assertNull(A.getParent()); // OK
        assertEquals(B.getParent().getName(), "A"); // FAILS (null pointer exception)! 
        assertEquals(C.getParent().getName(), "A"); // OK
    }
}

The test is set up to use the embedded driver. The log output at the "breakpoints" are as follows:

In order to keep the noise down, I've limited myself to include the log output that I think could be related to the problem. Please ask for more output in the comments if you need it. The same thing goes with configuration etc.

Breakpoint 0: Strange warning.

WARN: No identity field found for class of type: com.example.NodeTest when creating persistent property for field: private com.example.NodeRepository com.example.NodeTest.repository

Breakpoint 1: Node A is created.

INFO: Request: UNWIND {rows} as row CREATE (n:`node`) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type with params {rows=[{nodeRef=-1965998569, type=node, props={name=A}}]}

Breakpoint 2: Node B and its relationship to A is created.

INFO: Request: UNWIND {rows} as row CREATE (n:`node`) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type with params {rows=[{nodeRef=-1715570484, type=node, props={name=B}}]}
INFO: Request: UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`PARENT`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type with params {rows=[{startNodeId=1, relRef=-1978848273, type=rel, endNodeId=0}]}

Breakpoint 3: Node C and its relationship to A is created. But B's relationship to A is also deleted!

INFO: Request: UNWIND {rows} as row CREATE (n:`node`) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type with params {rows=[{nodeRef=-215596349, type=node, props={name=C}}]}
INFO: Request: UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`PARENT`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type with params {rows=[{startNodeId=2, relRef=-2003500348, type=rel, endNodeId=0}]}
INFO: Request: UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MATCH (startNode)-[rel:`PARENT`]->(endNode) DELETE rel with params {rows=[{startNodeId=1, endNodeId=0}]}

Breakpoint 4: Querying the repository.

INFO: Request: MATCH (n:`node`) WHERE n.`name` = { `name` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n) with params {name=A}
WARN: Cannot map iterable of class com.example.Node to instance of com.example.Node. More than one potential matching field found.
INFO: Request: MATCH (n:`node`) WHERE n.`name` = { `name` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n) with params {name=B}
INFO: Request: MATCH (n:`node`) WHERE n.`name` = { `name` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n) with params {name=C}

I suspect the problem to be connected to the warning in the second line (of "breakpoint 4"), but I don't understand the reason/solution for it.

Why can't the field be mapped? Why does this cause the semantics shown above? How do you correctly model a tree where you can traverse the parent-child relationship both ways?

Additional information:

If I remove the children field, the test passes. Reversing the direction of the relationship or making the field type (or subtype of) Collection doesn't make any difference.

The relevant project dependencies are org.springframework.boot:spring-boot-starter-data-neo4j:jar:1.4.0.RELEASE:compile, org.neo4j:neo4j-ogm-test:jar:2.0.4:test, and org.neo4j.test:neo4j-harness:jar:3.0.4:test.

1 Answer 1

1

When you have an incoming @Relationship, you must annotate the field, accessor and mutator methods with the @Relationship with type and direction INCOMING.

Secondly, I believe Iterable for children will not work with the OGM mapping process- implementations of List,Vector,Set,SortedSet will.

We have an example of a Tree here: https://github.com/neo4j/neo4j-ogm/blob/2.0/core/src/test/java/org/neo4j/ogm/domain/tree/Entity.java and the test https://github.com/neo4j/neo4j-ogm/blob/2.0/core/src/test/java/org/neo4j/ogm/persistence/examples/tree/TreeIntegrationTest.java

Edit:

So I've taken a look at the code again- Iterable will probably work. Might actually be a Set. Regarding your comment on parent.children.add(this), it is required because without it, your object model is not in sync with what you expect the graph model to be. When the OGM maps this, it could find that the child has a parent, but the parent does not include the child- and so it will pick one or the other as the source of truth.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks, but the children field has no accessor nor mutator methods: The problem is caused by its mere presence. Adding (annotated) getter and setter methods and/or making the type of the field List, Set or whatever doesn't solve the problem. (Btw, it seems unnecessarily redundant to have to annotate everything - do you know why this should be necessary?)
In the Entity I see that parent.children.add(this) is called from setParent, which (at first glance) appear to solve the problem. I guess this makes sense now that the field is mutable. But I don't understand why it doesn't work to make the field Iterable - shouldn't this be valid according to "... read-only fields are Iterable<T> ..."?
I don't understand what you mean that Iterable probably works. The whole question is about why it doesn't work, which the test proves that it doesn't. If the field is read-only, then there should be no conflict. What am I missing?
Your comment says " I see that parent.children.add(this) is called from setParent, which (at first glance) appear to solve the problem". So if that solves your issue, then it was not the Iterable. My answer points to a couple of areas that might contribute to your issue. Perhaps I have not understood what has fixed your issue or if it is still an issue
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.