如何避免在@ManyToMany 中保存重复项,但插入到映射 table 中?

How to avoid saving duplicates in @ManyToMany, but insert into mapping table?

我有 2 个实体 Post 和 Post 具有多对多关系的标签。 我还有 table 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 和 table post_tag_mapping,例如 post_id tag_id - (1, 1), (1, 2), (1, 3), 等等

但是如果我用数据库中已经存在的 post_tag 名称保存 post - 我不想将它保存到 post_tag(我在 post_tag.name 上有唯一索引), 但为另一个 post.

创建新的 post_tag_mapping

现在我得到异常 SQLIntegrityConstraintViolationException: Duplicate entry 'tag1' for key 'post_tag.idx_post_tag_name'

不太明白如何实现。

如果我很了解你的困境,你的问题是你在保存你的 Post 实体时试图插入新的 PostTag

由于 CascadeType.ALL,您正在执行级联保存,因此您的 EntityManager 大致是这样做的:

  • 节省Post
  • 保存Post标签哪个导致你的异常
  • 正在保存 Post <-> Post标签

你应该

  1. 有一项服务(例如:PostTag findOrCreateTagByName(String))可以按名称获取现有 PostTag 并最终创建它们。因此返回现有的 PostTag.
  2. 在与所述 现有 标签关联后保存 Post

编辑(作为对评论的回答):

JPA 只是到关系数据库的映射。

在你的代码中,你只显示了一个映射,它表明 Post 链接到多个 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 的唯一性约束,这意味着您插入了两次相同的值。要么在同一笔交易中,要么因为标签已经存在于 table.

例如:

post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));

如果您没有正确定义 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 上执行保存,然后在 Post 标签上执行保存,然后在关系 Post <-> [=138= 上执行保存]标签。

在 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") 将不再位于其有效存储桶中,您将无法再次找到它。

如果您在设置后不使用该对象,这可能没有问题,但我认为最好先保存 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;
}

并掌握语法。

并在 repo 中处理 eager fetch。

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = { "tags" })
    List<Post> findAll();
}