Spring Data JPA带额外字段的多对多关系:关联删除失败问题
问题:Spring Data JPA中无法删除Event与Supplier的关联关系
我在Spring Data JPA中用EventSupplier作为关联实体实现了Event与Supplier的多对多关系,该实体包含表示供应商单价的unit_value额外字段。保存事件时供应商及其单价可正常存储,更新现有供应商单价也能顺利完成,但删除事件与供应商的关联关系时却无法生效,请求协助解决。
简化代码
Event实体
@Entity @Table(name = "event") @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class Event { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) @JsonIgnore private Set<EventSupplier> eventSupplier = new HashSet<>(); @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Event event = (Event) o; return Objects.equals(id, event.id); } @Override public int hashCode() { return Objects.hash(id); } }
Supplier实体
@Entity @Table(name = "supplier") @Getter @Setter public class Supplier { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "supplier", cascade = CascadeType.ALL) @JsonIgnore private Set<EventSupplier> eventSupplier = new HashSet<>(); @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Supplier sup = (Supplier) o; return Objects.equals(id, sup.id); } @Override public int hashCode() { return Objects.hash(id); } }
EventSupplier关联实体
@Entity @Table(name = "event_supplier") @Getter @Setter public class EventSupplier { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL) @JoinColumn(name = "event_id") @JsonIgnore private Event event; @ManyToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL) @JoinColumn(name = "supplier_id") @JsonIgnore private Supplier supplier; @Column(name = "unit_value") @NotNull private BigDecimal unitValue; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; EventSupplier es = (EventSupplier) o; return Objects.equals(id, es.id); } @Override public int hashCode() { return Objects.hash(id); } }
更新事件的业务方法
@Transactional public Event updateEvent(EventUpdateDto dto, Long id) { Event e = eventRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Evento não encontrado!")); // Dto中的供应商集合 Set<Supplier> suppliers = supplierRepository.findAllByIdIn(dto.suppliers().keySet()); Set<Servant> servants = servantRepository.findAllByIdIn(dto.servants()); if (suppliers.isEmpty()) { throw new RuntimeException("O evento deve ter fornecedores ligados a ele!"); } if (servants.isEmpty()) { throw new RuntimeException("O evento deve ter servidores ligados a ele!"); } e.getServants().stream() .filter(servant -> !servants.contains(servant)) .forEach(servant -> servant.getEvents().remove(e)); e.setServants(servants); servants.forEach(s -> s.getEvents().add(e)); // 与事件关联的供应商列表 List<Supplier> supsEvent = e.getEventSupplier() .stream() .map(EventSupplier::getSupplier) .collect(Collectors.toList()); // 删除不在Dto供应商列表中的现有关联供应商 for (Supplier sup : supsEvent) { if (!suppliers.contains(sup)) { List<EventSupplier> esDelete = e.getEventSupplier() .stream() .filter(es -> es.getSupplier().equals(sup)) .toList(); e.getEventSupplier().removeAll(esDelete); sup.getEventSupplier().removeAll(esDelete); } } // 若关联不存在则添加新的供应商关联 for (Supplier sup : suppliers) { if (!e.getEventSupplier().stream().anyMatch(es -> es.getSupplier().equals(sup))) { EventSupplier newEventSupplier = new EventSupplier(); newEventSupplier.setSupplier(sup); newEventSupplier.setEvent(e); e.getEventSupplier().add(newEventSupplier); } } // 更新现有关联的单价 for (Supplier sup : suppliers) { e.getEventSupplier().stream() .filter(es -> es.getSupplier().equals(sup)) .findFirst() .ifPresent(es -> es.setUnitValue(dto.suppliers().get(sup.getId()))); } e.setTotalValue(dto.totalValue()); eventRepository.save(e); return e; }
问题原因与修复方案
原因分析
- 关联关系两端角色混淆:
Event和Supplier的@OneToMany使用了mappedBy,属于关联关系的Inverse端(非拥有方),而EventSupplier是拥有方(持有外键字段)。JPA仅会同步拥有方的状态变化,仅从Inverse端的集合中移除元素不会触发数据库删除操作。 - Cascade配置错误:
EventSupplier中@ManyToOne的cascade = CascadeType.ALL存在风险,会导致删除关联实体时误删Event或Supplier主实体,同时Inverse端的cascade配置无法触发关联实体的删除。
修复步骤
1. 修正EventSupplier的Cascade配置
将@ManyToOne的cascade改为仅包含必要操作,避免误删主实体:
@ManyToOne(fetch = FetchType.LAZY, optional = false, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinColumn(name = "event_id") @JsonIgnore private Event event; @ManyToOne(fetch = FetchType.LAZY, optional = false, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinColumn(name = "supplier_id") @JsonIgnore private Supplier supplier;
2. 显式删除关联实体EventSupplier
首先创建EventSupplierRepository:
public interface EventSupplierRepository extends JpaRepository<EventSupplier, Long> { }
修改updateEvent方法中的删除逻辑,显式删除关联实体:
// 删除不在Dto供应商列表中的现有关联供应商 for (Supplier sup : supsEvent) { if (!suppliers.contains(sup)) { List<EventSupplier> esDelete = e.getEventSupplier() .stream() .filter(es -> es.getSupplier().equals(sup)) .toList(); // 显式删除数据库中的关联记录 eventSupplierRepository.deleteAll(esDelete); // 维护内存中对象的集合关系 e.getEventSupplier().removeAll(esDelete); sup.getEventSupplier().removeAll(esDelete); } }
3. 优化集合遍历(可选)
使用Iterator遍历集合,避免并发修改异常:
// 遍历当前事件的所有关联供应商实体 Iterator<EventSupplier> iterator = e.getEventSupplier().iterator(); while (iterator.hasNext()) { EventSupplier es = iterator.next(); if (!suppliers.contains(es.getSupplier())) { eventSupplierRepository.delete(es); iterator.remove(); es.getSupplier().getEventSupplier().remove(es); } }
关键说明
- JPA仅同步关联关系拥有方的状态变化,Inverse端的集合操作仅用于维护内存对象一致性,不会自动触发数据库操作。
- 显式删除
EventSupplier实体是确保数据库关联记录被移除的可靠方式。 - 修正Cascade配置可避免误删主实体的风险。
内容的提问来源于stack exchange,提问作者Lucas_dev
相关产品推荐
相关产品推荐

