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

Spring Boot JPA软删除无法级联至子实体的问题解决

Spring Boot 级联软删除实现方案(含删除原因存储)

问题根源

你当前的实现有两个核心问题:

  1. 原生SQL更新绕过了JPA持久化上下文,无法触发CascadeType.ALL定义的级联操作,导致子实体不会被软删除
  2. @SQLDelete只能定义静态SQL,无法动态传递deletionReason参数

解决方案

我们通过基于JPA实体操作的软删除逻辑实现级联效果,同时统一管理软删除字段,确保查询自动过滤已删除数据。

1. 统一软删除基类(可选但推荐)

将deleted字段抽成基类,避免重复代码:

@MappedSuperclass
public abstract class SoftDeleteable {
    @Column(name = "deleted", nullable = false, columnDefinition = "BOOLEAN DEFAULT false")
    private Boolean deleted = false;

    // Getter & Setter
    public Boolean getDeleted() {
        return deleted;
    }

    public void setDeleted(Boolean deleted) {
        this.deleted = deleted;
    }
}

2. 修改实体类

所有需要软删除的实体继承SoftDeleteable,并添加@Where注解自动过滤已删除数据:

PersonalDetail 实体

@Entity
@Table(name="personal_details")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@Where(clause = "deleted = false")
public class PersonalDetail extends Auditable<String> implements Serializable, SoftDeleteable {
    private static final long serialVersionUID = 1L;

    @Id
    private String idNo;

    private String gender;
    private String name;
    private String deletionReason; // 删除原因字段

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id_no", referencedColumnName = "idNo")
    private Address address;

    @JsonManagedReference
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "personalDetail", cascade = CascadeType.ALL)
    private List<DevicesReceived> devicesReceivedList;

    @JsonManagedReference
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "personalDetail", cascade = CascadeType.ALL)
    private List<DevicesRequired> devicesRequiredList;

    @JsonManagedReference
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "personalDetail", cascade = CascadeType.ALL)
    private List<Benefit> benefitList;
}

Address 实体

@Entity
@Table(name = "address")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@Where(clause = "deleted = false")
public class Address extends Auditable<String> implements Serializable, SoftDeleteable {
    private static final long serialVersionUID = 1L;
    
    @Id
    private String idNo;
    private String address;
}

DevicesReceived 实体

@Entity
@Table(name="devices_received")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@Where(clause = "deleted = false")
public class DevicesReceived extends Auditable<String> implements Serializable, SoftDeleteable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Integer id;

    private String idNo;
    private String device;

    @JsonBackReference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "personal_details_id_no", nullable = false)
    private PersonalDetail personalDetail;
}

3. 优化Repository

移除原生SQL方法,添加实体图查询避免懒加载问题:

@Repository
public interface PersonalDetailRepository extends JpaRepository<PersonalDetail, String> {
    // 一次性加载所有关联实体,避免N+1查询
    @EntityGraph(attributePaths = {"address", "devicesReceivedList", "devicesRequiredList", "benefitList"})
    Optional<PersonalDetail> findById(String idNo);

    // 如需查询已删除数据,可添加此方法
    List<PersonalDetail> findByDeletedTrue();
}

4. 实现级联软删除逻辑(Service层)

通过JPA实体操作触发级联更新:

@Service
public class PersonalDetailService {

    private final PersonalDetailRepository personalDetailRepository;

    public PersonalDetailService(PersonalDetailRepository personalDetailRepository) {
        this.personalDetailRepository = personalDetailRepository;
    }

    @Transactional
    public void softDelete(String idNo, String deletionReason) {
        // 加载父实体及关联子实体
        PersonalDetail personalDetail = personalDetailRepository.findById(idNo)
                .orElseThrow(() -> new RuntimeException("未找到ID为" + idNo + "的个人信息"));
        
        // 设置父实体软删除状态及原因
        personalDetail.setDeleted(true);
        personalDetail.setDeletionReason(deletionReason);

        // 级联设置子实体软删除状态
        if (personalDetail.getAddress() != null) {
            personalDetail.getAddress().setDeleted(true);
        }
        personalDetail.getDevicesReceivedList().forEach(device -> device.setDeleted(true));
        personalDetail.getDevicesRequiredList().forEach(req -> req.setDeleted(true));
        personalDetail.getBenefitList().forEach(benefit -> benefit.setDeleted(true));

        // 保存父实体,JPA自动级联更新所有关联子实体
        personalDetailRepository.save(personalDetail);
    }
}

核心说明

  • 级联生效原理:通过JPA持久化上下文操作实体,触发CascadeType.ALL定义的级联更新,所有关联子实体的状态变更会被同步到数据库
  • 事务保障:@Transactional确保所有软删除操作在同一个事务中,避免部分更新成功的情况
  • 懒加载处理:@EntityGraph一次性加载所有关联实体,避免事务外访问懒加载集合抛出异常
  • 自动过滤:@Where注解确保默认查询不会返回已软删除的数据,无需业务代码手动过滤

内容的提问来源于stack exchange,提问作者Joe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 04:32:16