Spring Data JPA:期望更新中间表记录却插入新条目问题
解决Spring Data JPA多对多中间实体重复插入问题
问题原因
当前代码直接创建新的PurchasablePackageTag列表替换原有关联,但原有关联记录并未被删除,且JPA持久化上下文仍会保留旧的关联实体,最终导致重复插入。同时,中间实体缺少唯一约束,数据库层面无法阻止重复记录生成。
解决方案
1. 清理原有关联(两种方式)
方式一:利用JPA孤儿移除自动删除
在PurchasablePackage的purchasablePackageTags字段上添加级联配置,让JPA自动删除"孤儿"实体:
@OneToMany(mappedBy = "purchasablePackage", cascade = CascadeType.ALL, orphanRemoval = true) private List<PurchasablePackageTag> purchasablePackageTags;
修改edit方法,先清空原有关联再添加新关联:
@Override public PurchasablePackage edit(PurchasablePackageDto dto) { PurchasablePackage purchasablePackage = super.findOne(dto.getId()); purchasablePackage.setName(dto.getName()); // 清空原有关联(orphanRemoval=true会自动删除数据库中对应记录) List<PurchasablePackageTag> existingTags = purchasablePackage.getPurchasablePackageTags(); // 同步维护Tag侧的双向关联,避免状态不一致 for (PurchasablePackageTag tag : existingTags) { tag.getTag().getPackageTags().remove(tag); } existingTags.clear(); // 添加新的标签关联 List<Tag> tags = tagService.findTagsByIdIn(dto.tagIds); tags.forEach(tag -> { PurchasablePackageTag packageTag = PurchasablePackageTag.builder() .purchasablePackage(purchasablePackage) .tag(tag) .displayDate(new Date()) // 根据业务需求设置日期 .build(); existingTags.add(packageTag); // 维护Tag侧的双向关联 tag.getPackageTags().add(packageTag); }); return this.repository.save(purchasablePackage); }
方式二:手动删除原有关联记录
创建PurchasablePackageTagRepository并添加删除方法:
public interface PurchasablePackageTagRepository extends JpaRepository<PurchasablePackageTag, Long> { void deleteByPurchasablePackageId(Long packageId); }
在edit方法中先删除旧关联,再添加新关联:
@Autowired private PurchasablePackageTagRepository packageTagRepository; @Override public PurchasablePackage edit(PurchasablePackageDto dto) { PurchasablePackage purchasablePackage = super.findOne(dto.getId()); purchasablePackage.setName(dto.getName()); // 手动删除该套餐的所有旧标签关联 packageTagRepository.deleteByPurchasablePackageId(purchasablePackage.getId()); // 添加新的标签关联 List<PurchasablePackageTag> purchasablePackageTags = new ArrayList<>(); List<Tag> tags = tagService.findTagsByIdIn(dto.tagIds); tags.forEach(tag -> { PurchasablePackageTag packageTag = PurchasablePackageTag.builder() .purchasablePackage(purchasablePackage) .tag(tag) .displayDate(new Date()) .build(); purchasablePackageTags.add(packageTag); }); purchasablePackage.setPurchasablePackageTags(purchasablePackageTags); return this.repository.save(purchasablePackage); }
2. 数据库层面添加唯一约束
为PurchasablePackageTag添加联合主键或唯一约束,从根源阻止重复插入。例如使用@EmbeddedId定义联合主键:
@Embeddable public class PurchasablePackageTagId implements Serializable { @Column(name = "fk_purchasable_package") private Long packageId; @Column(name = "fk_tag") private Long tagId; // 必须实现equals和hashCode方法,确保主键判断逻辑正确 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PurchasablePackageTagId that = (PurchasablePackageTagId) o; return Objects.equals(packageId, that.packageId) && Objects.equals(tagId, that.tagId); } @Override public int hashCode() { return Objects.hash(packageId, tagId); } } // 在PurchasablePackageTag中使用该主键 @Entity public class PurchasablePackageTag { @EmbeddedId private PurchasablePackageTagId id; @Column(name = "display_date") private Date displayDate; @ManyToOne @MapsId("packageId") // 关联到嵌入式主键的packageId字段 @JoinColumn(name = "fk_purchasable_package") private PurchasablePackage purchasablePackage; @ManyToOne @MapsId("tagId") // 关联到嵌入式主键的tagId字段 @JoinColumn(name = "fk_tag") private Tag tag; // 构造函数、getter、setter方法 }
关键注意事项
- 双向关联需同步维护:确保
Tag和PurchasablePackage两侧的关联都被正确更新,避免实体状态不一致。 - 级联操作谨慎使用:
CascadeType.ALL会将所有持久化操作级联到关联实体,需根据业务场景调整范围。
内容的提问来源于stack exchange,提问作者Mehdi Rahimi
相关产品推荐
相关产品推荐

