我在 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 ...
}
task_tag 表确实在这两列(且仅有)上形成了一个复合索引。
我做错了什么?我遵循了每一个建议和建议 - 使用集合而不是列表,使用辅助方法,清理等...... 我找不到错误。
谢谢!
答案 0 :(得分:0)
最让我印象深刻的是你的 Tag 的 equals 和 hash-code 彼此不匹配。
您的“等于”基于对象的名称相同来驱动相等,这对于“标签就是名称”的口头禅是合乎逻辑的。但是基于“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 的关系和身份/等号/hashCode:The JPA hashCode() / equals() dilemma
答案 1 :(得分:-2)
请在多对多注解的cascade属性中添加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();
}
}