JPA - Mapping Composite Primary Key OneToMany

Viewed 194

Suppose there are 2 entities, which represent a blog Post and its comments. One blog can have multiple comments:

@Entity
public class Post {

   @Id
   @GeneratedValue
   private Long id;

   private String name;

   @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "post")
   private List<Comment> comments;
}

@Entity
@IdClass(CommentPK.class)
public class Comment {

  @Id
  private Long id;  

  @Id
  @ManyToOne
  private Post post;

  private String content;
}

class CommentPK implements Serializable {
   private Long post;
   private Long id;
}

I want Comment to have composite PK of (post_id, comment_id), where comment_id is a sequence within one post, for example:

| post_id | comment_id | content  |
| 1       | 1          | 123      |
| 1       | 2          | 456      |
| 1       | 3          | Hello SO |
| 2       | 1          | New Post | << Post #2 has comment IDs starting from 1 again

While mapping works, I can not save post, because comment does not have ID. This is why I've added entity listeners:

@PrePersist
@PreUpdate
public void preProcess(Post post) {
  int id = 1;
  for (Comment comment : post.getComments()) {
    comment.setPost(post);
    comment.setId(id++);
  }
}

It works on the first post persist, however if I add new comments to persisted post, entity listener is not called. I have also tried adding entity listener for Comment with the similar logic, but it is just not not invoked

@Test
public void testPersistAndUpdate() {
  Post post = new Post();
  post.setName("Test");

  Comment comment1 = new Comment();
  comment1.setPost(post);
  comment1.setContent("Comment 1");

  List<Comment> comments = new ArrayList<>();
  comments.add(comment1);
  post.setComments(comments);

  Post savedPost = postRepository.saveAndFlush(post); // Spring Data JPA repository, all OK for now
  
  // create new Comment
  Comment comment2 = new Comment();
  comment2.setPost(post);
  comment2.setContent("Comment 2");

  savedPost.getComments().add(comment2);
  savedPost = postRepository.saveAndFlush(savedPost); // here entity listener is not invoked and exception is thrown

}

I am getting an exception:

javax.persistence.PersistenceException: org.hibernate.HibernateException: No part of a composite identifier may be null

Questions

  1. What is the common practice of modeling relations like these? (still keeping composite key a requirement) As it feels pretty uncomfortable dealing with it through JPA/Hibernate now

  2. If keeping the mapping from the example, is it possible to make @PrePersist for Comment working, when Post attributes are not updated, but the Comment is new and should be persisted?


1 Answers

I do not think that usage of JPA Callbacks is a proper approach for your problem (See also this article). I would suggest you to use @MapsId annotation. You can correct the mapping in the following way:

@Entity
public class Post {

   @Id
   @GeneratedValue
   private Long id;

   @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "post")
   private List<Comment> comments;
}

@Entity
public class Comment {

  @EmbeddedId
  CommentPK id;
  

  @MapsId("post")
  @ManyToOne
  private Post post;

  // 
}

@Embeddable
class CommentPK implements Serializable {
   private Long post;
   private Long id;
   
   // getters, setters, equals, hashCode
}

The CommentPK.post value will be derived from the Comment.post association.

Related