[英]How to do unidirectional many-to-many relationships with already existing registers?
我是 Spring/JPA 的新手,我正在嘗試創建一種與 Vlad 的這篇文章非常相似的關系,但有一個區別。 我的標簽已存在於另一個表中。
因此,如果我像 Vlad 在其帖子中所做的那樣,創建一個帖子,為其添加一些標簽,然后將其持久化,那么一切都會按預期進行。 我在 Post 上有一個注冊,兩個在 Tag 上,兩個在 PostTag 上。
Post newPost = new Post("Title");
newPost.addTag(new Tag("TagName"));
newPost.addTag(new Tag("TagName2"));
this.postRepository.save(newPost);
但是,如果我在創建帖子之前嘗試創建標簽並保存它,則會出現錯誤。
Tag tag = new Tag("TagAlreadyCreated");
this.tagRepository.save(tag);
Post newPost = new Post("Title");
newPost.addTag(tag);
this.postRepository.save(newPost);
// Error: detached entity passed to persist: com.***.***.Tag
我知道我不想創建標簽,如果它已經存在,並且分離的消息意味着我的標簽已經有一個 ID,所以我嘗試將 CascadeType 更改為 MERGE,但隨后我沒有創建注冊在 PostTag 上。 類的代碼:
郵政
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostTag> tags = new ArrayList<>();
public Post() {
}
public Post(String title) {
this.title = title;
}
public void addTag(Tag tag) {
PostTag postTag = new PostTag(this, tag);
tags.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.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 Objects.equals(title, post.title);
}
@Override
public int hashCode() {
return Objects.hash(title);
}
public Long getId() {
return id;
}
}
標簽
@Entity(name = "Tag")
@Table(name = "tag")
@NaturalIdCache
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Tag {
@Id
@GeneratedValue
private Long id;
public Long getId() {
return id;
}
@NaturalId
private String name;
public Tag() {
}
public Tag(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
Tag tag = (Tag) o;
return Objects.equals(name, tag.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
郵戳
@Entity(name = "PostTag")
@Table(name = "post_tag")
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;
@Column(name = "created_on")
private Date createdOn = new Date();
private PostTag() {}
public void setPost(Post post) {
this.post = post;
}
public void setTag(Tag tag) {
this.tag = tag;
}
public PostTag(Post post, Tag tag) {
this.post = post;
this.tag = tag;
this.id = new PostTagId(post.getId(), tag.getId());
}
//Getters and setters omitted for brevity
@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);
}
public Post getPost() {
return post;
}
public Tag getTag() {
return tag;
}
@Override
public int hashCode() {
return Objects.hash(post, tag);
}
}
郵政標簽ID
@Embeddable
public class PostTagId
implements Serializable {
@Column(name = "post_id")
private Long postId;
@Column(name = "tag_id")
private Long tagId;
private PostTagId() {}
public PostTagId(
Long postId,
Long tagId) {
this.postId = postId;
this.tagId = tagId;
}
//Getters omitted for brevity
@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);
}
}
spring-data-jpa是JPA之上的一層。 每個實體都有其自己的存儲庫,您必須處理該存儲庫。 我已經看過上面提到的教程,它是針對JPA的,並且還將ID設置為null,這似乎有點不正確,可能是導致錯誤的原因。 我沒那么近看。 為了處理spring-data-jpa中的問題,您需要為鏈接表提供單獨的存儲庫。
@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);
}
並使用它,如上所示:
@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);
}
請注意,更改很少。 我沒有明確設置PostTagId
ID。 這些由持久層(在這種情況下為休眠)處理。
還請注意,由於設置了CascadeType.ALL
因此可以使用其自己的存儲庫來顯式更新PostTag
條目,也可以通過從列表中添加和刪除PostTag
條目來進行顯示,如圖所示。 將CascadeType.ALL
用於spring-data-jpa的問題在於,即使您預取了聯接表實體,spring-data-jpa仍將再次執行此操作。 嘗試通過CascadeType.ALL
為新實體更新關系是有問題的。
如果沒有CascadeType
則posts
或tags
列表(應為Set)都不是該關系的所有者,因此,在持久性方面添加到它們將不會完成任何事情,並且僅用於查詢結果。
在讀取PostTag
關系時,由於沒有FetchType.EAGER
需要專門獲取它們。 這個問題FetchType.EAGER
是開銷,如果你不想加入,也如果你把它放在倆都Tag
和Post
那么你將創建一個遞歸獲取該得到所有Tags
和Posts
的任何查詢。
@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);
最后,請始終檢查日志。 請注意,創建關聯需要spring-data-jpa(我認為是JPA)讀取現有表以查看該關系是新建的還是更新的。 無論您自己創建和保存PostTag
還是預取列表, PostTag
發生這種情況。 JPA有單獨的合並,我認為您可以更有效地使用它。
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 (?, ?)
我想我找到了答案。
首先要知道的是,如果您將open-in-view 設置為 true ,Spring 將在請求的整個生命周期內保持 JPA/Hibnernate 會話打開。 這意味着您的代碼(比方說在服務方法中)應該可以正常工作。 當然這不是很有效(看看這里為什么):https ://vladmihalcea.com/the-open-session-in-view-anti-pattern/
現在,如果您將open-in-view 設置為 false ,Spring 似乎會為每個存儲庫調用打開一個新會話。 因此,一個用於獲取標簽的新會話,然后是一個用於保存帖子的新會話。 這就是為什么在保存帖子時標簽實體被分離的原因。
要解決此問題,您需要使用@Transactional注釋您的服務調用。 Spring 將嘗試在事務內重用相同的會話,因此實體不會分離。 例如,請參見此處: @Transactional 如何影響 Hibernate 中的當前會話?
最后要知道哪個是至關重要的,服務方法必須是公開的! 如果您對方法有任何其他可見性,@Transactional 將被忽略,則不會引發任何錯誤: https : //docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-聲明性注釋(參見方法可見性和@
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.