Reputation: 784
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
Upvotes: 1
Views: 7488
Reputation: 11551
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.
@Entity
public class Post {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTag> tags;
@Entity
public class Tag {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTag> posts;
@Entity
public class PostTag {
@EmbeddedId
private PostTagId id = new PostTagId();
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("postId")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("tagId")
private Tag tag;
public PostTag() {}
public PostTag(Post post, Tag tag) {
this.post = post;
this.tag = tag;
}
@SuppressWarnings("serial")
@Embeddable
public class PostTagId implements Serializable {
private Long postId;
private Long tagId;
@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);
}
And to use it, as show above:
@Transactional
private void update() {
System.out.println("Step 1");
Tag tag1 = new Tag();
Post post1 = new Post();
PostTag p1t1 = new PostTag(post1, tag1);
tagRepo.save(tag1);
postRepo.save(post1);
postTagRepo.save(p1t1);
System.out.println("Step 2");
Tag tag2 = new Tag();
Post post2 = new Post();
PostTag p2t2 = new PostTag(post2, tag2);
postRepo.save(post2);
tagRepo.save(tag2);
postTagRepo.save(p2t2);
System.out.println("Step 3");
tag2 = tagRepo.getOneWithPosts(2L);
tag2.getPosts().add(new PostTag(post1, tag2));
tagRepo.save(tag2);
System.out.println("Step 4 -- better");
PostTag p2t1 = new PostTag(post2, tag1);
postTagRepo.save(p2t1);
}
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 since CascadeType.ALL
is set, as shown. The problem with using the CascadeType.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 the CascadeType.ALL
for new entities is problematic.
Without the CascadeType
neither the posts
or tags
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 have FetchType.EAGER
. The problem with FetchType.EAGER
is the overhead if you don't want the joins and also if you put it on both Tag
and Post
then you will create a recursive fetch that gets all Tags
and Posts
for any query.
@Query("select t from Tag t left outer join fetch t.posts tps left outer join fetch tps.post where t.id = :id")
Tag getOneWithPosts(@Param("id") Long id);
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.
create table post (id bigint generated by default as identity, primary key (id))
create table post_tag (post_id bigint not null, tag_id bigint not null, primary key (post_id, tag_id))
create table tag (id bigint generated by default as identity, primary key (id))
alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post
alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag
Step 1
insert into tag (id) values (null)
insert into post (id) values (null)
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)
Step 2
insert into post (id) values (null)
insert into tag (id) values (null)
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)
Step 3
select tag0_.id as id1_2_0_, posts1_.post_id as post_id1_1_1_, posts1_.tag_id as tag_id2_1_1_, post2_.id as id1_0_2_, posts1_.tag_id as tag_id2_1_0__, posts1_.post_id as post_id1_1_0__ from tag tag0_ left outer join post_tag posts1_ on tag0_.id=posts1_.tag_id left outer join post post2_ on posts1_.post_id=post2_.id where tag0_.id=?
select tag0_.id as id1_2_1_, posts1_.tag_id as tag_id2_1_3_, posts1_.post_id as post_id1_1_3_, posts1_.post_id as post_id1_1_0_, posts1_.tag_id as tag_id2_1_0_ from tag tag0_ left outer join post_tag posts1_ on tag0_.id=posts1_.tag_id where tag0_.id=?
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)
Step 4 -- better
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)
Upvotes: 6