Logical cyclic mapping dependency

Hi, yesterday I ran into a problem while creating an account in the application I'm currently developing. The console log says:

org.springframework.data.mapping.MappingException: The node with id 5 has a logical cyclic mapping dependency. Its creation caused the creation of another node that has a reference to this.

Classes related:

@Node("Tree")
class Tree(
    @Relationship(type = "OWNS_TREE", direction = Relationship.Direction.INCOMING)
    val owner: Person
) {
    @Id
    @GeneratedValue
    var id: Long = -1

    ...
}
@Node("Person")
class Person(
    var firstName: String,   
    ) {
    @Id
    @GeneratedValue
    var personId: Long = -1

    @Relationship(type = "IS_CHILD_OF", direction = Relationship.Direction.OUTGOING)
    var parents: MutableSet<Person> = mutableSetOf()

    @Relationship(type = "IS_CHILD_OF", direction = Relationship.Direction.INCOMING)
    var children: MutableSet<Person> = mutableSetOf()

    @Relationship(type = "IS_SIBLING_OF")
    var siblings: MutableSet<Person> = mutableSetOf()

    @Relationship(type = "IS_PARTNER_OF")
    var partners: MutableSet<Person> = mutableSetOf()

    @Relationship(type = "OWNS_TREE")
    var ownedTree: Tree? = null

I know that this is because in Person there is a OWNS_TREE relationship, that is also in Tree, which would make it a cyclic dependency, but what does it mean exactly?
The second question is how should I process to having the relationship between mentioned classes that way and working on them without getting that kind of an exception?

The problem is rooted in the dependency chain (cycle) Tree -> Person -> Tree, as you have already discovered.
Spring Data Neo4j has a dead-lock detection during mapping to avoid stack overflow / infinite recursion if it comes to cyclic mapping dependencies. This is why you see the exception.

What happens in detail?

  1. SDN starts mapping the Tree that depends on a Person instance (constructor) and puts it in a set of "objects in construction".
  2. The Person instance gets created AND the properties of it populated. This has to be done because a property can -besides public visibility and setter- also be "set" by using a wither method (Spring Data Neo4j)
  3. The ownedTree property needs the Tree-in-construction -> Exception

To avoid this, you can either remove the constructor dependency or the bi-directional relationship definition.

1 Like

I have a variant of the same issue:

Node → Node

@Builder
@Value
@Node
public class NodeEntity {
    @Id
    @GeneratedValue
    @With
    UUID uuid;

    @Version
    @With
    Long version;

    // properties ...

    @Relationship(type = "LINKS", direction = Direction.OUTGOING)
    List<LinkEntity> linksOut;
}


@Builder
@Value
@RelationshipProperties
public class LinkEntity {
    @RelationshipId
    Long id;

    // properties ...

    @TargetNode
    NodeEntity target;
}

My question is: assuming my application needs to support such cyclical dependencies, what is the best way to avoid this error?

...has a logical cyclic mapping dependency; its creation caused the creation of another node that has a reference to this

@gerrit.meier mentioned

  • modifying the relationship -- seems like a non-starter if I need to model such cyclical relationships
  • removing the constructor dependency -- I'm not sure what this means, or how I might go about experimenting with it. Does this suggest instantiating node objects without their relations / removing the relation property (linksOut) from the constructor definition?

Many thanks for any help,
Ross

It's not impossible to do this, but Lombok's annotations create a all-args constructor for the @Value. In this case you have the Node1-><anything or nothing>->Node1 dependency in the mapping process. Since Node1 will require a mapping of its relationships to satisfy the constructor requirement, it eventually ends up at the point where it needs to map Node1 again in your case.
You are on the right track that you would have to remove the constructor dependency created by Lombok. I don't know if this is possible in combination with the @Value annotation but you would have to have at least a constructor and, in the case of multiple ones, mark it with @PersistenceConstructor.

Incredibly helpful—thank you so much, Gerrit.

I did try adding an all-but-relations constructor to both classes in my original code (annotated with @PersistenceConstructor / @PersistenceCreator), but had the same issue.

I got it working by doing this in both the @Node and @RelationshipProperties classes:

  • Replacing @Value with @Data, which creates a required-args constructor rather than all-args
  • Removing @With and @Builder annotations, both of which depend on an all-args constructor

Resulting code:

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Node("Node")
public final class NodeEntity {
    @Id
    @GeneratedValue
    UUID uuid;

    @Version
    Long version;

    // properties ...

    @Relationship(type = "LINKS", direction = Direction.OUTGOING)
    List<LinkEntity> linksOut;
}

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@RelationshipProperties
public class LinkEntity {
    @RelationshipId
    Long id;

    // properties ...

    @TargetNode
    NodeEntity target;
}

Thank you again.