簡體   English   中英

如何避免在@ManyToMany 中保存重復項,但插入到映射表中?

[英]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
  • 保存帖子 <-> PostTag

你應該

  1. 有一項服務(例如: PostTag findOrCreateTagByName(String) )可以按名稱獲取現有的PostTag並最終創建它們。 從而返回現有的PostTag
  2. 在與所述現有標簽關聯后保存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);

該代碼將執行:

  • 查找標簽 A:它在數據庫中,如PostTag(1, "A")
  • 查找標簽 B:它在數據庫中,如PostTag(2, "B")
  • 找到標簽 C:它不在數據庫中,創建它。

這應該會起作用,因為級聯將在 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()確保單個PostPostTag的唯一性:

如果您在hashCode()中使用id (並且等於使用idname ):

  • 如果您使用id ,則該集合將具有PostTag(1, "A")PostTag(2, "B")PostTag("C")
  • 保存時,PostTag("C") 將分配一個 id -> PostTag(3, "C")
  • 使用標准HashSetPostTag("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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM