You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

问题原因与修复方案

原因分析

  1. 关联关系两端角色混淆:Event和Supplier的@OneToMany使用了mappedBy,属于关联关系的Inverse端(非拥有方),而EventSupplier是拥有方(持有外键字段)。JPA仅会同步拥有方的状态变化,仅从Inverse端的集合中移除元素不会触发数据库删除操作。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 11:48:14