我有 2 個具有多對多關系的物體 Post 和 PostTag。我也有帶有 post_id 和 post_tag_id 的表 post_tag_mapping。
@ManyToMany(cascade = {CascadeType.ALL})
@JoinTable(
name = "post_tag_mapping",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
@Getter
@Builder.Default
private Set<PostTag> postTagSet = new HashSet<>();
如果我創建帖子Set<PostTag>- 我保存帖子、1 個或多個 post_tag 和表 post_tag_mapping,如 post_id tag_id - (1, 1), (1, 2), (1, 3) 等。
但是,如果我使用資料庫中已經存在的 post_tag 名稱保存帖子 - 我不想將其保存到 post_tag(我在 post_tag.name 上有唯一索引),而是為另一個帖子創建新的 post_tag_mapping。
現在我得到例外SQLIntegrityConstraintViolationException: Duplicate entry 'tag1' for key 'post_tag.idx_post_tag_name'
不太明白如何實作它。
uj5u.com熱心網友回復:
PostTag如果我很好地理解您的困境,那么您的問題是您在保存Post物體時試圖插入新的。
由于由于 CascadeType.ALL,您正在執行級聯保存,因此您的 EntityManager 大致執行以下操作:
- 保存帖子
- 保存導致您的例外的PostTag
- 保存帖子 <-> PostTag
你應該
- 有一個服務(例如:),它按名稱
PostTag findOrCreateTagByName(String)獲取現有PostTag的并最終創建它們。從而回傳現有的PostTag. - 保存
Post后與所述現有標簽的關聯。
編輯(作為評論的答案):
JPA 只是到關系資料庫的映射。
在您的代碼中,您僅顯示表示 aPost鏈接到多個PostTag(并且PostTag鏈接到多個Post)的映射。
您添加了適用于所有標簽的唯一約束:在所有資料庫中,必須有一個標簽“A”,一個標簽“B”,依此類推。
如果你像這樣填充你的物件(我不使用 lombok,所以我在這里假設一個最小的建構式):
Post post = new Post();
post.setXXX(...);
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("B"));
這意味著您將創建兩個名為 A 和 B 的新標簽。
JPA 實作(Hibernate、EclipseLink)并不神奇:它們不會為您獲取現有標簽以及它將失敗的標簽。如果您違反了 table 的唯一性約束post_tag,這意味著您插入了兩次相同的值。要么在同一個事務中,要么因為標簽已經存在于表中。
例如:
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));
如果您沒有正確定義hashCode(),則只會使用物件標識 hashCode 并且會嘗試添加(插入)兩個標簽A。
您在這里唯一可以做的就是PostTag通過正確實施來限制 ,hashCode()/equals以便僅PostTagSet確保相關 Post 的唯一性。
現在假設您首先獲取它們并擁有一個新標簽C:
Post post = new Post();
post.setXXX(...);
for (String tagName : asList("A", "B", "C")) {
post.getPostTagSet().add(tagRepository.findByName(tagName)
.orElseGet(() -> new PostTag(tagName ));
}
postRepository.save(post);
這tagRepository只是一個 Spring JPA 存盤庫——我認為你正在使用它——findByName 簽名是:
Optional<String> findByName(String tagName);
該代碼將執行以下操作:
- 查找標簽 A:它在資料庫中,如
PostTag(1, "A") - 查找標簽 B:它在資料庫中,如
PostTag(2, "B") - 查找標簽 C:它不在資料庫中,創建它。
This should then work because the cascade will perform a save on the Post, then on the PostTag, then on the relation Post <-> PostTag.
In term of SQL query, you should normally see something like this:
insert into post_tag (tag_id, name) (3, "C")
insert into post (post_id, ...) (<some id>, ...)
insert into post_tag_mapping (tag_id, post_id) (1, <some id>)
insert into post_tag_mapping (tag_id, post_id) (2, <some id>)
insert into post_tag_mapping (tag_id, post_id) (3, <some id>)
The other problem here is with the hashCode() and equals() provided by PostTag which ensure unicity of PostTag for one single Post:
If you use the id in hashCode() (and equals use id and name):
- If you use the
id, then the set will havePostTag(1, "A"),PostTag(2, "B")andPostTag("C") - When you save, PostTag("C") will have an id assigned -> PostTag(3, "C")
- With standard
HashSet, thePostTag("C")will no longer be in its valid bucket and you will fail to find it again.
如果您在集合之后不使用該物件,這可能不會有問題,但我認為最好先保存PostTag(為其分配一個 id)然后將其添加到集合中。
如果使用nameinhashCode()和equals: 只要插入 set 后不更新名稱,就沒有問題。
uj5u.com熱心網友回復:
去做就對了。
@SpringBootApplication
public class DemoApplication implements ApplicationRunner{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
PostRepository postRepository;
@Autowired
PostTagRepository postTagRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
init();
testException("name");
addNewPost("name");
addNewPost("other");
readPosts();
}
private void init() {
postTagRepository.save(PostTag.builder().name("name").build());
}
private void testException(String name) {
try {
PostTag postTag = postTagRepository.save(PostTag.builder().name(name).build());
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
} catch ( DataIntegrityViolationException ex ) {
System.out.println("EX: " ex.getLocalizedMessage());
}
}
private void addNewPost(String name) {
PostTag postTag = postTagRepository.findByName(name)
.orElseGet(()->postTagRepository.save(PostTag.builder().name(name).build()));
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
}
private void readPosts() {
System.out.println(postRepository.findAll());
}
}
并且不要使用你不理解的東西
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id @GeneratedValue
private Long id;
@ManyToMany
private Set<PostTag> tags;
}
并獲得語法。
并在 repo 中處理 Eager fetch。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = { "tags" })
List<Post> findAll();
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/433490.html
