Neo4j Spring同类型实体双向KNOWS关系建模及栈溢出问题咨询
Let's break down what's wrong with your current setup and fix it step by step.
What's Wrong With Your Current Modeling?
Your approach is causing a circular reference loop that triggers a StackOverflowError during persistence:
- When you save
user2, Spring Data Neo4j (SDN) processes its outgoingknowsset, which referencesuser1. - SDN then processes
user1's incomingknownByset, which referencesuser2. - This back-and-forth recursion repeats infinitely until the call stack is exhausted.
Additionally, maintaining separate knows (outgoing) and knownBy (incoming) collections with @RelationshipProperties is unnecessary for most use cases—Neo4j relationships are inherently traversable in both directions without needing duplicate physical relationships.
Better Implementation Options
Option 1: Logical Mutual Relationship (Recommended)
If you just need to traverse the relationship in both directions (not create two separate physical relationships), you only need to maintain one direction of the relationship in your entity. Neo4j lets you query relationships regardless of their defined direction.
Updated Entity Code
@Node public class User { @Id @GeneratedValue private UUID id; private String name; private String phoneNumber; private String email; private boolean isActive; // Only maintain outgoing KNOWS relationships with properties @JsonIgnore @Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING) private Set<Knows> knows = new HashSet<>(); public User() { } // Helper to add a KNOWS relationship public void addKnows(User targetUser, String relationshipLabel) { Knows relationship = new Knows(relationshipLabel, Instant.now(), targetUser); this.knows.add(relationship); } // Getters/setters for fields (omitted for brevity) } @RelationshipProperties public class Knows { private String as; @CreatedDate private Instant createdAt; @JsonIgnore @TargetNode private User targetUser; public Knows(String as, Instant createdAt, User targetUser) { this.as = as; this.createdAt = createdAt; this.targetUser = targetUser; } // Getters/setters (omitted for brevity) }
Saving the Relationship
User user1 = userRepository.findById(id1).orElseThrow(); User user2 = userRepository.findById(id2).orElseThrow(); // Create a single outgoing relationship from user2 to user1 user2.addKnows(user1, "friend"); userRepository.save(user2);
Querying the Reverse Relationship
To find who knows user1, use a custom repository query instead of maintaining an incoming collection:
@Repository public interface UserRepository extends Neo4jRepository<User, UUID> { @Query("MATCH (knownUser:User)<-[:KNOWS]-(user:User) WHERE knownUser.id = $userId RETURN user, KNOWS") List<User> findUsersWhoKnow(@Param("userId") UUID userId); }
Option 2: Physical Mutual Relationships (Two Separate Relationships)
If you truly need two distinct physical relationships (matching your original Cypher), avoid using entity collections to manage both directions—use a direct Cypher query instead to bypass the circular reference issue.
Custom Repository Method
@Repository public interface UserRepository extends Neo4jRepository<User, UUID> { @Query("MATCH (u1:User {id: $id1}), (u2:User {id: $id2}) " + "CREATE (u1)-[:KNOWS {as: $as, createdAt: $createdAt}]->(u2) " + "CREATE (u2)-[:KNOWS {as: $as, createdAt: $createdAt}]->(u1)") void createMutualKnowsRelationship( @Param("id1") UUID id1, @Param("id2") UUID id2, @Param("as") String as, @Param("createdAt") Instant createdAt ); }
Executing the Query
userRepository.createMutualKnowsRelationship(id1, id2, "friend", Instant.now());
Key Takeaways
- Avoid circular collections: Never maintain both incoming and outgoing relationship collections in entities when using
@RelationshipProperties—it triggers infinite recursion during persistence. - Leverage Neo4j's flexibility: Most "bidirectional" relationship needs can be solved with a single physical relationship and bidirectional queries, no duplicate relationships required.
- Use direct Cypher for edge cases: When you need duplicate physical relationships, bypass entity mapping and execute Cypher directly via repository methods.
内容的提问来源于stack exchange,提问作者Vimal23

