Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

enter image description here

The second time I run the unit test, duplicate relationships are being created:

enter image description here

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.

like image 425
chocksaway Avatar asked Oct 20 '25 17:10

chocksaway


1 Answers

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.

like image 200
David Pond Avatar answered Oct 23 '25 06:10

David Pond



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!