Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive relationship in Spring Data and JPA?

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;
}
like image 808
Jasmine Rain Avatar asked Dec 10 '22 06:12

Jasmine Rain


2 Answers

@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.

like image 51
Jasmine Rain Avatar answered Dec 23 '22 11:12

Jasmine Rain


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()));
like image 38
K.Nicholas Avatar answered Dec 23 '22 11:12

K.Nicholas