I've been struggling to make a many to many relationship with an additional column in the link table.
These are my entities:
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Post {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private List<PostTag> tags = new ArrayList<>();
//getters and setters
public void addTag(Tag tag){
PostTag postTag = new PostTag(this, tag);
tags.add(postTag);
tag.getPosts().add(postTag);
}
public void removeTag(Tag tag) {
for (Iterator<PostTag> iterator = tags.iterator();
iterator.hasNext(); ) {
PostTag postTag = iterator.next();
if (postTag.getPost().equals(this) &&
postTag.getTag().equals(tag)) {
iterator.remove();
postTag.getTag().getPosts().remove(postTag);
postTag.setPost(null);
postTag.setTag(null);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Post post = (Post) o;
return id == post.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Tag {
@Id
@GeneratedValue
private Long id;
private String comment;
@OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private List<PostTag> posts = new ArrayList<>();
//getters and setters
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Tag that = (Tag) o;
return id == that.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@Entity(name = "PostTag")
@Table(name = "post_tag")
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class PostTag {
@EmbeddedId
private PostTagId id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("postId")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("tagId")
private Tag tag;
private Integer impact;
public FacilityParticipant(Post post, Tag tag) {
this.post = post;
this.tag = tag;
this.id = new PostTagId(post.getId(), tag.getId());
}
//getters and setters
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
PostTag that = (PostTag) o;
return Objects.equals(post, that.post) && Objects.equals(tag, that.tag);
}
@Override
public int hashCode() {
return Objects.hash(post, tag);
}
}
@Embeddable
public class PostTagId implements Serializable {
private Long postId;
private Long tagId;
//getters setters
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
PostTagId that = (PostTagId) o;
return Objects.equals(postId, that.postId) && Objects.equals(tagId, that.tagId);
}
@Override
public int hashCode() {
return Objects.hash(postId, tagId);
}
}
I have a post entity that is mapped to many tags and a tag that is mapped to many posts. The link table is PostTag which contains the mappings to both sides, and the additional column, "impact". The PK of the link table is mapped to an Embeddable table PostTagId, which contains the PK from Post and Tag.
When I try to insert new entities, I do the following:
Tag tag1 = new Tag();
Tag tag2 = new Tag();
repository.save(tag1);
repository.save(tag2);
Post post1 = new Post();
Post post2 = new Post();
post1.addTag(tag1);
post1.addTag(tag2);
post2.addTag(tag1);
repository.save(post1);
repository.save(post2);
When trying to insert these items, I get the error that I cannot insert NULL into ("POST_TAG"."ID")
Anything that I've tried, it either comes with other errors, or it gets right back at it.
Most probably something from the model is not right, but I really cannot figure what is wrong with it.
The whole modelling was based on this article The best way to ...
Any help would be really appreciated.
Thanks
The spring-data-jpa is a layer on top of JPA. Each entity has its own repository and you have to deal with that. I've seen that tutorial mentioned above and it's for JPA and it's also setting ID's to null which seems off a bit and probably the cause of your error. I didn't look that close. For dealing with the issue in spring-data-jpa you need a separate repository for the link table.
And to use it, as show above:
Note there are few changes. I don't explicitly set the
PostTagId
id's. These are handled by the persistence layer (hibernate in this case).Note also that you can update
PostTag
entries either explicity with its own repo or by adding and removing them from the list sinceCascadeType.ALL
is set, as shown. The problem with using theCascadeType.ALL
for spring-data-jpa is that even though you prefetch the join table entities spring-data-jpa will do it again anyway. Trying to update the relationship through theCascadeType.ALL
for new entities is problematic.Without the
CascadeType
neither theposts
ortags
lists (which should be Sets) are the owners of the relationship so adding to them wouldn't accomplish anything in terms of persistence and would be for query results only.When reading the
PostTag
relationships you need to specifically fetch them since you don't haveFetchType.EAGER
. The problem withFetchType.EAGER
is the overhead if you don't want the joins and also if you put it on bothTag
andPost
then you will create a recursive fetch that gets allTags
andPosts
for any query.Finally, always check the logs. Note that creating an association requires spring-data-jpa (and I think JPA) to read the existing table to see if the relationship is new or updated. This happens whether you create and save a
PostTag
yourself or even if you prefetched the list. JPA has a separate merge and I think you can use that more efficiently.