[英]How to avoid saving duplicates in @ManyToMany, but insert into mapping table?
我有 2 個具有多對多關系的實體 Post 和 PostTag。 我還有表 post_tag_mapping 與 post_id 和 post_tag_id。
@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>
創建 Post - 我會保存 post、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'
不太明白如何實現它。
如果我很了解您的困境,那么您的問題是您在保存Post
實體時試圖插入新的PostTag
。
由於您由於 CascadeType.ALL 而進行級聯保存,因此您的 EntityManager 大致是這樣做的:
你應該
PostTag findOrCreateTagByName(String)
)可以按名稱獲取現有的PostTag
並最終創建它們。 從而返回現有的PostTag
。Post
。編輯(作為評論的答案):
JPA 只是到關系數據庫的映射。
在您的代碼中,您只顯示映射,該映射表示一個Post
鏈接到多個PostTag
(並且PostTag
鏈接到多個Post
)。
您添加了一個適用於所有標簽的唯一約束:在您的所有數據庫中,必須有一個標簽“A”,一個標簽“B”,依此類推。
如果你像這樣填充你的 object (我不使用 lombok,所以我在這里假設一個最小的構造函數):
Post post = new Post();
post.setXXX(...);
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("B"));
這意味着您創建了兩個名為 A 和 B 的新標簽。
JPA 實現(Hibernate、EclipseLink)並不神奇:它們不會為您獲取現有標簽,也不會獲取失敗的標簽。 如果您違反了表post_tag
的唯一性約束,這意味着您插入了兩次相同的值。 要么在同一個事務中,要么因為標簽已經存在於表中。
例如:
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));
如果您沒有正確定義hashCode()
,那么只會使用 object 身份 hashCode 並且會嘗試添加(插入)兩個標簽A
。
您在這里唯一可以做的就是通過正確實施hashCode()/equals
來限制PostTag
,以便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);
該代碼將執行:
PostTag(1, "A")
PostTag(2, "B")
這應該會起作用,因為級聯將在 Post 上執行保存,然后在 PostTag 上執行保存,然后在關系 Post <-> PostTag 上執行保存。
對於 SQL 查詢,您通常應該看到如下內容:
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>)
這里的另一個問題是PostTag
提供的hashCode()
和equals()
確保單個Post
的PostTag
的唯一性:
如果您在hashCode()
中使用id
(並且等於使用id
和name
):
id
,則該集合將具有PostTag(1, "A")
、 PostTag(2, "B")
和PostTag("C")
HashSet
, PostTag("C")
將不再位於其有效存儲桶中,您將無法再次找到它。 如果您在設置后不使用 object,這可能沒有問題,但我認為最好先保存PostTag
(為其分配一個 ID),然后將其添加到設置中。
如果在hashCode()
和equals
中使用name
:只要在插入集合后不更新名稱,就不會有問題。
去做就對了。
@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;
}
並獲得語法。
並在回購中處理急切獲取。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = { "tags" })
List<Post> findAll();
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.