I have an issue with a many-to-many relation in Spring Boot. Code is as follows:
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();
  }
}
and
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();
  }
}
Of course, I have the task_tag table where, after inserting a tag in a task and saving that task, an entry appears. However, when I delete a tag (or clear them), the entries do not get deleted from the join table. This is the test:
@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 ...
  }
I delete tag1 but when I try to add it back to the task, I get

The task_tag table does have a composite index formed on those two (and only) columns.
What am I doing wrong? I followed each and every suggestion and advice - using set instead of lists, having helper methods, cleaning up etc... I can't find the bug.
Thank you!
 
     
    