Querying with Bidirectional Relationship with Spring Boot

I have a Person model that includes children and parent relationship. I am using one relationship (HAS_CHILD) to represent both direction like below.

// @Data
@Getter
@AllArgsConstructor
@Node
public class Person {
    @Id @GeneratedValue @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC)
    private Long id;

    private String firstName;
    private String lastName;

    // HAVING BOTH OF THESE RELATIONSHIPS CAUSES CYLCES IN THE GRAPH
    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private Set<Person> children; //using set bc its unordered and has no duplicates. 

    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private Set<Person> parents; //using set bc its unordered and has no duplicates. 
}

I have this repository:

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends Neo4jRepository<Person, Long> {
    List<Person> findByFirstNameStartsWithIgnoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName);

    // List<Long> findIdByFirstNameStartsWithIgnoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName);

    @Query(
        "MATCH (refNode:Person WHERE ID(refNode) = 0) -[r_parent:HAS_CHILD*]-> (child:Person)" +
        "MATCH (refNode) <-[r_child:HAS_CHILD*]- (parent)" +
        "RETURN refNode, collect(r_parent), collect(child), collect(parent)"
    )
    List<Person> findChildrenAndParents(@Param("id") Long id);
}

Here is the error:

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.data.mapping.MappingException: Error mapping Record<{refNode: node<0>, collect(r_parent): [[relationship<3>], [relationship<3>], [relationship<3>]], collect(child): [node<2>, node<2>, node<2>], collect(parent): [node<7>, node<6>, node<5>]}>] with root cause

org.springframework.data.mapping.MappingException: The node with id 4:b55ce53a-2669-4e5b-bda1-00e6033cf3d5:0 has a logical cyclic mapping dependency; its creation caused the creation of another node that has a reference to this

This particular query works in neo4j:


I want to get the same output from Neo4j in Spring Boot JSON, but this logical cyclic dependency is an issue.
How can I fix this?
Thanks!

The problem is not the bidirectional relationship in this case but that you define an @AllArgsConstructor on the entity.
Simple example: When creating Person1 SDN needs to create Person2 first but this depends on Person1 again which is already "in creation".
My advice is to move the relationship field (at least one direction) into a setter instead of a constructor argument.

1 Like

Thanks for the quick response. I tried changing @AllArgsConstructor to just @Setter which didn't throw any error, but it also does not work anymore. The result I get in JSON was either "id": 0 or nothing at all.
Here is my code:

// @Getter
@Setter
// @AllArgsConstructor
// @Data
@Node
public class Person {
    @Id @GeneratedValue @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC)
    private Long id;

    private String firstName;
    private String lastName;

    // HAVING BOTH OF THESE RELATIONSHIPS CAUSES CYLCES IN THE GRAPH
    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private List<Person> children; //using set bc its unordered and has no duplicates. 

    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private List<Person> parents; //using set bc its unordered and has no duplicates. 
}

My expected results are supposed to match the neo4j model in nested JSON. But, I get "id":0 for 0, and [ ] for any other id provided:

Here is my Person repo again:

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends Neo4jRepository<Person, Long> {
    List<Person> findByFirstNameStartsWithIgnoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName);

    @Query(
        "MATCH (refNode:Person WHERE ID(refNode) = $id) -[r_parent:HAS_CHILD*]-> (child:Person)" +
        "MATCH (refNode) <-[r_child:HAS_CHILD*]- (parent)" +
        "RETURN refNode, collect(r_parent), collect(child), collect(parent)"
    )
    List<Person> findChildrenAndParents(@Param("id") Long id);

If I use @Data instead (chose this bc it doesn't have an @AllArgsConstructor to it), I get an infinite loop error in my terminal. However, SwaggerUI doesn't know how to respond and returns Error: response status is 200.

// @Getter
// @Setter
// @AllArgsConstructor
@Data
@Node
public class Person {
    @Id @GeneratedValue @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC)
    private Long id;

    private String firstName;
    private String lastName;

    // HAVING BOTH OF THESE RELATIONSHIPS CAUSES CYLCES IN THE GRAPH
    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private List<Person> children; //using set bc its unordered and has no duplicates. 

    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private List<Person> parents; //using set bc its unordered and has no duplicates. 
}

Here is the looping error:

  at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18) ~[jackson-databind-2.15.3.jar:2.15.3]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[jackson-databind-2.15.3.jar:2.15.3]

and it keeps doing that for all of my functions.

Thank you again for the help, please let me know what could be the issue.

You are missing the r_child in your return pattern. I don't know if it is needed from the structure you are trying to map, but without this, SDN won't map the parent.

The resulting error is now Jackson that fails to render the json object because you are indefinitely chasing the relationships. For this to work, you need to instruct Jackson to skip a property. This can either be done via @JsonIgnore for the instance itself or on the related instance via @JsonIgnoreProperties (disclaimer: there might be more ways, but those are the ones that stuck in my head).
Examples:
@JsonIgnore will ignore every incoming relationship HAS_CHILD

public class Person {

    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private List<Person> children; //using set bc its unordered and has no duplicates. 

    @JsonIgnore
    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private List<Person> parents; 
}

@JsonIgnoreProperties will ignore the back-reference in the parent

public class Person {

    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private List<Person> children; //using set bc its unordered and has no duplicates. 

    @JsonIgnoreProperties("children")
    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private List<Person> parents; 
}
1 Like

Thank you for the help! @JsonIgnoreProperties("children") on the parent field gave me my expected results.

I had to change my repo function for findChildrenAndParents because my edge cases were null:
@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends Neo4jRepository<Person, Long> {
List findByFirstNameStartsWithIgnoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName);

@Query(
    "MATCH (refNode:Person WHERE ID(refNode) = $id)" +
    "OPTIONAL MATCH (refNode) -[r_parent:HAS_CHILD*]-> (child:Person)" +
    "OPTIONAL MATCH (refNode) <-[r_child:HAS_CHILD*]- (parent:Person)" +
    "RETURN refNode, collect(r_parent), collect(child), collect(r_child), collect(parent)"
)
List<Person> findChildrenAndParents(@Param("id") Long id);

I want create immutable objects with @Value annotation, so I can't use setters. But it gives us the same mapping exception error as before.

What should we do?

Person.java

package com.example.neo4jdatabasepractice.model;

import java.util.Date;
import java.util.List;

import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.Relationship.Direction;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Builder;
import lombok.Value;

@Value
@Builder(toBuilder = true)
@Node
public class Person {

    public static final String HAS_CHILD_RELATIONSHIP_NAME = "HAS_CHILD";

    @Id
    @GeneratedValue
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    @JsonIgnoreProperties(value = "parents")
    @Relationship(type = "HAS_CHILD", direction = Direction.OUTGOING)
    private List<Person> children;
    @JsonIgnoreProperties(value = "children")
    @Relationship(type = "HAS_CHILD", direction = Direction.INCOMING)
    private List<Person> parents;
}

If you really want to have immutable support, you could use the wither-concept of Spring Data. Spring Data Object Mapping Fundamentals :: Spring Data Neo4j

There is also Lombok support for this: Wither (Lombok)

You only need to define withers for the other Persons not for every field.

I tried using @With, but I still get the logical dependency error. I also did not really understand what you meant by "defining withers for other Persons not for every field."

Please let me know where I went wrong.

Here's my Person model:

@Value
@Builder(toBuilder = true)
@With
@Node
public class Person {

    public static final String HAS_CHILD_RELATIONSHIP_NAME = "HAS_CHILD";

    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    
    @JsonIgnoreProperties(value = "parents")
    @Relationship(type = HAS_CHILD_RELATIONSHIP_NAME, direction = Direction.OUTGOING)
    private List<Person> children;
    @JsonIgnoreProperties(value = "children")
    @Relationship(type = HAS_CHILD_RELATIONSHIP_NAME, direction = Direction.INCOMING)
    private List<Person> parents;

Here's the error:

Error mapping Record<{refNode: node<6>, collect(r_parent): [[relationship<2>], [relationship<5>], [relationship<34>], [relationship<34>, relationship<35>], [relationship<34>, relationship<35>, relationship<36>]], collect(child): [node<1>, node<3>, node<7>, node<0>, node<2>], collect(r_child): [], collect(parent): []}>] with root cause

org.springframework.data.mapping.MappingException: The node with id 4:b55ce53a-2669-4e5b-bda1-00e6033cf3d5:6 has a logical cyclic mapping dependency; its creation caused the creation of another node that has a reference to this

You still need to find a way to not use @Value in your case but all annotation except the @AllArgsConstructor. This is what leads to the problem, because you still have a constructor dependency from Person to Person. Maybe @Data is the right one if you make all other variables besides the relationship final.

Also, sorry that I linked the wrong API. It should have been @With and good that you took this instead of the deprecated, experimental one.

Hey Gerrit!
I wasn't able to figure out a way to use @Value or @Data since I am unable to have an @AllArgsConstructor because of my childrens and parents relationship.
And since @Builder and @With uses @AllArgsConstructor, I wasn't able to use those.
Unless you have a suggestion, I will return to using mutable setters and just .set() my nodes to update them.

Thanks for the help though!