【问题标题】:Spring boot - many to many association not removing join table dataSpring boot - 多对多关联不删除连接表数据
【发布时间】:2021-07-07 17:41:15
【问题描述】:

我在 Spring Boot 中遇到了多对多关系的问题。代码如下:


public class Task {

  @Id
  @GeneratedValue
  private Long id;

  @ManyToMany(cascade = {PERSIST, MERGE}, fetch = EAGER)
  @JoinTable(
      name = "task_tag",
      joinColumns = {@JoinColumn(name = "task_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id")}
  )
  @Builder.Default
  private Set<Tag> tags = new HashSet<>();

  public void addTags(Collection<Tag> tags) {
    tags.forEach(this::addTag);
  }

  public void addTag(Tag tag) {
    this.tags.add(tag);
    tag.getTasks().add(this);
  }

  public void removeTag(Tag tag) {
    tags.remove(tag);
    tag.getTasks().remove(this);
  }

  public void removeTags() {
    for (Iterator<Tag> iterator = this.tags.iterator(); iterator.hasNext(); ) {
      Tag tag = iterator.next();
      tag.getTasks().remove(this);
      iterator.remove();
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Task)) return false;
    return id != null && id.equals(((Task) o).getId());
  }

  @Override
  public int hashCode() {
    return id.intVal();
  }
}

public class Tag {

  @Id
  @GeneratedValue
  private Long id;

  @NotNull
  @Column(unique = true)
  private String name;

  @ManyToMany(cascade = {PERSIST, MERGE}, mappedBy = "tags", fetch = EAGER)
  @Builder.Default
  private final Set<Task> tasks = new HashSet<>();

  @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 id.intVal();
  }

}

当然,我有task_tag 表,在任务中插入标签并保存该任务后,会出现一个条目。但是,当我删除标签(或清除它们)时,条目不会从连接表中删除。这是测试:

@Test
  void entityIntegration() {
    Task task = taskRepo.save(...);

    Tag tag1 = Tag.builder().name(randomString()).build();
    Tag tag2 = Tag.builder().name(randomString()).build();
    Tag tag3 = Tag.builder().name(randomString()).build();
    Tag tag4 = Tag.builder().name(randomString()).build();
    final List<Tag> allTags = Arrays.asList(tag1, tag2, tag3, tag4);
    tagRepo.saveAll(allTags);

    task.addTag(tag1);
    taskRepo.save(task);
    final Long task1Id = task.getId();
    assertTrue(tag1.getTasks().stream().map(Task::getId).collect(Collectors.toList()).contains(task1Id));

    task.clearTags();
    task = taskRepo.save(task);
    tag1 = tagRepo.save(tag1);
    assertTrue(task.getTags().isEmpty());
    assertTrue(tag1.getTasks().isEmpty());

    task.addTags(allTags);
    task = taskRepo.save(task); // FAILS, duplicate key ...

  }

我删除了 tag1,但是当我尝试将它添加回任务时,我得到了

task_tag 表确实在这两个(也是唯一的)列上形成了一个复合索引。

我做错了什么?我遵循了每一个建议和建议 - 使用集合而不是列表,使用辅助方法,清理等...... 我找不到错误。

谢谢!

【问题讨论】:

  • getClass().hashCode()(即一个常量)是您可能从对象的 hashCode 实现中返回的最差值
  • 无关,但我改了。
  • task.addTags(allTags) 行之后,如果在 foreach 循环中打印task.getTags(),有多少个?你在使用Lists 时遇到过同样的问题吗?另外,当您从 Tag.tasks 映射中删除 cascade = {PERSIST, MERGE} 时,我会检查它是否有效?
  • 为什么需要双向映射?我建议标签不应该知道这些任务。无论如何,您确定您的 clearTagsremoveTags 实现工作正常,即它从任务集中正确删除了任务?如果将对象添加到基于哈希的集合中,例如您的情况,则在 equals/hashCode 中使用 id 可能会出现问题。尝试在持久化后完全重新加载数据(即在生成和设置 id 之后)
  • 是的,那些辅助方法确实可以正常工作

标签: spring spring-boot hibernate jpa many-to-many


【解决方案1】:

让我印象深刻的最大问题是您的标签的等号和哈希码彼此不匹配。

您的“equals”基于对象的名称相同来驱动相等,这对“A tag is a name”的口头禅是合乎逻辑的。但是基于“id”的哈希码驱动是等效的,根本不使用名称。

暂时忘记 JPA/Hibernate,当这两者不同步时,只是普通的旧集合本身会变得非常不可预测。

您可以在此处阅读更多相关信息,特别是为什么不匹配相等性的哈希码最终会散列到错误的存储桶并导致混淆,将其全部保存在 HashSets 中:Why do I need to override the equals and hashCode methods in Java?

有很多方法可以让它们重新同步(使用 Lombok 之类的库和 IDE 中的代码生成工具),但我不会指定一个,而是简单地指出这个网络资源,方便,为他的示例创建了一个具有完全相同概念的标签,因此我怀疑您可以自己使用完全相同的模式。

https://vladmihalcea.com/the-best-way-to-use-the-manytomany-annotation-with-jpa-and-hibernate/

这是我发现的另一个有用的 SO 线程,它讨论了关系和身份/等于/哈希码,因为它会影响 JPA:The JPA hashCode() / equals() dilemma

【讨论】:

    【解决方案2】:

    请在多对多注释的级联属性中添加 DELETE 关键字。而且我相信您对 Tag 类的任务属性的注释应该如下更改。

    你可以试试下面的映射

    
    public class Task {
    
      @Id
      @GeneratedValue
      private Long id;
    
      @ManyToMany(cascade = {PERSIST, MERGE,DELETE}, fetch = EAGER)
      @JoinTable(
          name = "task_tag",
          joinColumns = {@JoinColumn(name = "task_id", referencedColumnName = "id")},
          inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id")}
      )
      @Builder.Default
      private Set<Tag> tags = new HashSet<>();
    
      public void addTags(Collection<Tag> tags) {
        tags.forEach(this::addTag);
      }
    
      public void addTag(Tag tag) {
        this.tags.add(tag);
        tag.getTasks().add(this);
      }
    
      public void removeTag(Tag tag) {
        tags.remove(tag);
        tag.getTasks().remove(this);
      }
    
      public void removeTags() {
        for (Iterator<Tag> iterator = this.tags.iterator(); iterator.hasNext(); ) {
          Tag tag = iterator.next();
          tag.getTasks().remove(this);
          iterator.remove();
        }
      }
    
      @Override
      public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Task)) return false;
        return id != null && id.equals(((Task) o).getId());
      }
    
      @Override
      public int hashCode() {
        return id.intVal();
      }
    }
    
    public class Tag {
    
      @Id
      @GeneratedValue
      private Long id;
    
      @NotNull
      @Column(unique = true)
      private String name;
    
      @ManyToMany(cascade = {PERSIST, MERGE,DELETE}, mappedBy = "tags", fetch = EAGER)
    @JoinTable(
          name = "task_tag",
          joinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id")},
          inverseJoinColumns = {@JoinColumn(name = "task_id", referencedColumnName = "id")}
      )
      @Builder.Default
      private final Set<Task> tasks = new HashSet<>();
    
      @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 id.intVal();
      }
    
    }
    
    

    【讨论】:

    • 它不能解决问题,并且肯定会添加 REMOVE(而不是 DELETE),这不是我想要的。我不希望在删除与项目关联的标签时删除我的项目。
    猜你喜欢
    • 2021-07-12
    • 1970-01-01
    • 2012-02-17
    • 2018-07-04
    • 2011-03-15
    • 2014-03-09
    • 2019-12-20
    • 1970-01-01
    • 2020-03-29
    相关资源
    最近更新 更多