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.