Recursive relationship in Spring Data and JPA?

12,468

Solution 1

@ManyToOne
@JoinColumn(referencedColumnName = "id")
@JsonBackReference
private Comment parentComment;

I added @JsonBackReference that solved the java.lang.IllegalStateException: Cannot call sendError() after the response has been committed error. The parent Comment is also able to see the subComments set.

Solution 2

You seem to be over complicating it.

1) You have the @JoinColumn(referencedColumnName = "id") but that is redundant. That's what the foreign key references anyway so you don't need to be explicit about it. No worries, but don't write code you don't need.

2) If you have the parentId in the a new subComment you don't need to look it up and add it to the parent comment's list. The concept you're missing here is the "owning" entity concept. Look at the Javadoc for mappedBy. Since the parentComment field does the mapping it defines the owning entity. Granted, it's the same Entity, but the point is that it's the parentComment field that controls the persistence. You don't need to add anything to the Set of subComments in order for the relationship to be persisted. You can do so if you want but JPA will ignore it. You only need to set the parentComment field. E.g.

Edit: This example is with JPA instead of Spring Data, but it's the same under the hood.

Your entity needs only be:

@Entity
public class Comment {
    @Id @GeneratedValue
    private Integer id;

    @OneToMany(mappedBy="parentComment")
    private Set<Comment> subComments;

    @ManyToOne
    private Comment parentComment;

and you use it like so:

private void run() {
    runWrite();
    runRead();
    Comment comment = new Comment();
    comment.setId(1);
    Comment subComment = new Comment();
    subComment.setParentComment(comment);
    runSaveSubComment(subComment);
    runRead();
}
private void runWrite() {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    tx = em.getTransaction();
    try {
        tx.begin();
        Comment comment = new Comment();
        Comment subComment = new Comment();
        subComment.setParentComment(comment);
        em.persist(comment);
        em.persist(subComment);

        tx.commit();
    } finally {
        emf.close();
    }        
}
private void runRead() {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    try {
        Comment comment = em.createQuery("select c from Comment c left join fetch c.subComments where c.id = :id", Comment.class).setParameter("id", 1).getSingleResult();
        System.out.println(comment + Arrays.toString( comment.getSubComments().toArray()) );
    } finally {
        emf.close();
    }
}
private void runSaveSubComment(Comment subComment) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    tx = em.getTransaction();
    try {
        tx.begin();
        em.persist(subComment);
        tx.commit();
    } finally {
        emf.close();
    }        
}

or if you want to do with Spring Data.

Comment comment = new Comment();
Comment sub1 = new Comment();
sub1.setParentComment(comment);
repo.save(comment);
repo.save(sub1);
Comment parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));
Comment sub2 = new Comment();
sub2.setParentComment(parentComment);
repo.save(sub2);
parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));
// or 
Comment p = new Comment();
p.setId(1);
Comment sub3 = new Comment();
sub3.setParentComment(p);
repo.save(sub3);
parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));
Share:
12,468
Jasmine Rain
Author by

Jasmine Rain

Updated on July 02, 2022

Comments

  • Jasmine Rain
    Jasmine Rain almost 2 years

    My Comment entity is self-joined that has a subComment set.

    @Entity
    public class Comment {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private String id;
    
    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
    private Set<Comment> subComments = new HashSet<>();
    
    @ManyToOne
    @JoinColumn(referencedColumnName = "id")
    private Comment parentComment;
    

    Here in my addComment method

    public ResponseEntity<Comment> addComment(Comment comment) {
        Comment currComment = commentRepository.save(comment);
        if (currComment.getParentId() != null) {
            Comment parent = commentRepository.findById(currComment.getParentId()).orElse(null);
            if (parent != null) {
                parent.addSubComment(currComment);
                currComment.setParentId(parent.getId());
                currComment.setParentComment(parent);
                commentRepository.save(parent);
            }
        }
        Comment responseComment = commentRepository.save(currComment);
        return ResponseEntity.ok(responseComment);
    }
    

    When I tried to establish the reverse relationship (owning side), comment.setParentComment(parent); causes the error

    comment.setParentComment(parent); is causing an error: java.lang.IllegalStateException: Cannot call sendError() after the response has been committed

    Full Comment entity class

    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
    private Set<Comment> subComments = new HashSet<>();
    
    @ManyToOne
    @JoinColumn(referencedColumnName = "id")
    private Comment parentComment;
    
    private boolean isParent;
    private String parentId;
    
    public String getParentId() {
        return parentId;
    }
    
    public void setParentId(String parentId) {
        this.parentId = parentId;
    }
    
    public Set<Comment> getSubComments() {
        return subComments;
    }
    
    public void setSubComments(Set<Comment> subComments) {
        this.subComments = subComments;
    }
    
    public Comment addSubComment(Comment comment) {
        this.subComments.add(comment);
        return this;
    }
    
    public Comment getParentComment() {
        return parentComment;
    }
    
    public void setParentComment(Comment parentComment) {
        this.parentComment = parentComment;
    }
    
    public boolean getIsParent() {
        return isParent;
    }
    
    public void setIsParent(boolean isParent) {
        this.isParent = isParent;
    }
    
  • Jasmine Rain
    Jasmine Rain almost 6 years
    Spot on with your comments. I'm using Spring boot though, is there any way to simplify?
  • K.Nicholas
    K.Nicholas almost 6 years
    I'm not, just using the id of the first parent that was written to the database which I happen to know is 1. Typically, the current Comment is sent to the view with its id and the id is returned from the view when a user adds a new sub-comment. So, instead of fetching the parent comment from the database with the id just make a new Comment and set the id from the view. The save() call doesn't care about any other fields of the parent comment so it saves you a database call. The spring example shows it being done both ways.
  • K.Nicholas
    K.Nicholas almost 6 years
    Also, the custom fetchSubCommentsById gets the parents AND the subComments, so be aware of that. Normally, if you just got a Comment from the database the subComments would not be filled out. If you tried to access them you would get a lazy initialization exception. The fetchSubCommentsById is not well named. I just copied the query from the JPA section to a custom method in the Repository interface. See fetchMode discussion at stackoverflow.com/questions/29602386/….
  • user625488
    user625488 almost 4 years
    @K Just a note, since it might be helpful for others: you can avoid the lazy init exception by annotating the method with @Transactional. Has something to do with the entities staying connected to the entity manager, allowing the entity manager to lazy load relations.