'Duplicate Relationships being created using Spring Data neo4j - SDN / ReactiveNeo4j (neo4j community edition 4.4.4)
I am implementing a basic node - [relationship] - node, using a NameEntity class, which has a Relationship(type="LINK", direction = INCOMING) annotation.
The Link class has a TargetNode NameEntity.
I have a unit test which creates three nodes, with two relationships. The first time I run the unit test, the nodes and relationships are created:
The second time I run the unit test, duplicate relationships are being created:
I'm pretty new to neo4j (i'm using the community version 4.4.4).
I'm not expecting the duplicate second relationship to be created (with the same Link type). I appreciate I am not using a Version property.
Is it the default behaviour for neo4j to create a second relationship (with the same attributes). Is there a way for this second (duplicate) relationship to not be created?
I have attached copied NameEntity, and Link pojo, the unit test, and the Cypher which is run (first and second) time I run the unit test.
I 'think' the solution may be to override equals, and hashCode, but a concrete example would be very welcome. I haven't managed to find one.
NameEntity:
@Node("Name")
@Getter
public class NameEntity {
@Id
private String name;
@Relationship(type = "LINK", direction = INCOMING)
private List<Link> nameLinks;
public NameEntity(final String name) {
this.name = name;
}
public NameEntity() {}
public void install(Link nameLink) {
if (nameLinks == null) {
nameLinks = new ArrayList<>();
}
nameLinks.add(nameLink);
}
}
Link (Relationship with TargetNode):
@RelationshipProperties
@Getter
public class Link {
@RelationshipId
private Long id;
private String value;
@TargetNode
private NameEntity nameEntity;
public Link(NameEntity nameEntity, String value) {
this.nameEntity = nameEntity;
this.value = value;
}
// equals and hashCode override does not work
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Link))
return false;
Link link = (Link) o;
return Objects.equals(value, link.value)
&& Objects.equals(nameEntity.getName(), link.nameEntity.getName());
}
@Override
public int hashCode() {
return Objects.hash(value);
}
public Link() {}
public NameEntity getNameEntity() {
return nameEntity;
}
public String getValue() {
return value;
}
}
Spring Boot first time running unit test:
2022-04-13 18:31:33.243 DEBUG 25444 --- [o-auto-1-exec-1] .d.n.c.t.ReactiveNeo4jTransactionManager : Creating new transaction with name [org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-04-13 18:31:33.464 WARN 25444 --- [o4jDriverIO-2-2] o.s.d.n.c.m.DefaultNeo4jIsNewStrategy : Instances of class com.chocksaway.neo4j.entity.NameEntity with an assigned id will always be treated as new without version property!
2022-04-13 18:31:33.529 DEBUG 25444 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:31:33.639 DEBUG 25444 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:31:33.655 DEBUG 25444 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MATCH (startNode:`Name`) WHERE startNode.name = $fromId MATCH (endNode) WHERE id(endNode) = $toId CREATE (startNode)<-[relProps:`LINK`]-(endNode) SET relProps += $__properties__ RETURN id(relProps)
2022-04-13 18:31:33.663 DEBUG 25444 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:31:33.669 DEBUG 25444 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MATCH (startNode:`Name`) WHERE startNode.name = $fromId MATCH (endNode) WHERE id(endNode) = $toId CREATE (startNode)<-[relProps:`LINK`]-(endNode) SET relProps += $__properties__ RETURN id(relProps)
2022-04-13 18:31:33.682 DEBUG 25444 --- [o4jDriverIO-2-2] .d.n.c.t.ReactiveNeo4jTransactionManager : Initiating transaction commit
2022-04-13 18:31:33.762 INFO 25444 --- [ionShutdownHook] o.neo4j.driver.internal.InternalDriver : Closing driver instance 2107393518
2022-04-13 18:31:33.764 INFO 25444 --- [ionShutdownHook] o.n.d.i.async.pool.ConnectionPoolImpl : Closing connection pool towards localhost:7687
Spring Boot second time running unit test:
2022-04-13 18:33:44.751 DEBUG 16572 --- [o-auto-1-exec-1] .d.n.c.t.ReactiveNeo4jTransactionManager : Creating new transaction with name [org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-04-13 18:33:44.981 WARN 16572 --- [o4jDriverIO-2-2] o.s.d.n.c.m.DefaultNeo4jIsNewStrategy : Instances of class com.chocksaway.neo4j.entity.NameEntity with an assigned id will always be treated as new without version property!
2022-04-13 18:33:45.044 DEBUG 16572 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:33:45.143 DEBUG 16572 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:33:45.160 DEBUG 16572 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MATCH (startNode:`Name`) WHERE startNode.name = $fromId MATCH (endNode) WHERE id(endNode) = $toId CREATE (startNode)<-[relProps:`LINK`]-(endNode) SET relProps += $__properties__ RETURN id(relProps)
2022-04-13 18:33:45.168 DEBUG 16572 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MERGE (nameEntity:`Name` {name: $__id__}) SET nameEntity += $__properties__ RETURN nameEntity
2022-04-13 18:33:45.185 DEBUG 16572 --- [o4jDriverIO-2-2] org.springframework.data.neo4j.cypher : Executing:
MATCH (startNode:`Name`) WHERE startNode.name = $fromId MATCH (endNode) WHERE id(endNode) = $toId CREATE (startNode)<-[relProps:`LINK`]-(endNode) SET relProps += $__properties__ RETURN id(relProps)
2022-04-13 18:33:45.211 DEBUG 16572 --- [o4jDriverIO-2-2] .d.n.c.t.ReactiveNeo4jTransactionManager : Initiating transaction commit
2022-04-13 18:33:45.290 INFO 16572 --- [ionShutdownHook] o.neo4j.driver.internal.InternalDriver : Closing driver instance 1728266914
2022-04-13 18:33:45.293 INFO 16572 --- [ionShutdownHook] o.n.d.i.async.pool.ConnectionPoolImpl : Closing connection pool towards localhost:7687
Unit Test:
@Test
public void testAddName() throws URISyntaxException {
RestTemplate restTemplate = new RestTemplate();
final String baseUrl = "http://localhost:"+randomServerPort+"/name";
final Link nameEntity1 = new Link(new NameEntity("name001", "Person"), "Link");
final Link nameEntity2 = new Link(new NameEntity("name002", "Person"), "Link");
final NameEntity bob = new NameEntity("Bob", "Person");
bob.setNameLinks(Set.of(nameEntity1, nameEntity2));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
final ResponseEntity<String> response = restTemplate.postForEntity(baseUrl, bob, String.class);
Assertions.assertEquals(200, response.getStatusCodeValue());
Assertions.assertTrue(response.hasBody());
Assertions.assertTrue(response.getBody().contains("Bob"));
}
Any help would be appreciated.
Thanks
Miles.
Solution 1:[1]
In Cypher typically MERGE is used to avoid creating duplicates in this way. If you look at your Spring Boot log you'll see the Nodes being created like this:
MERGE (nameEntity:`Name` {name: $__id__})
SET nameEntity += $__properties__
RETURN nameEntity
The use of MERGE not CREATE ensures your nodes aren't duplicated. You could also create a constraint that enforces unique "name" properties on Nodes but this would error when you try to create a duplicate.
Now if we look at the log for creating the relationships we see:
MATCH (startNode:`Name`)
WHERE startNode.name = $fromId
MATCH (endNode)
WHERE id(endNode) = $toId
CREATE (startNode)<-[relProps:`LINK`]-(endNode)
SET relProps += $__properties__
RETURN id(relProps)
This code finds (MATCH) the nodes and then creates (CREATE) the relationship. This should be a MERGE, to avoid duplication. I don't know how to make your framework output this Cypher though.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | David Pond |


