Spring Boot单向@OneToMany关联软删除时子实体ID置空问题
问题现象
在实现父实体与子实体的软删除逻辑时,双向关联(User-Task)的行为符合预期,但单向关联(Task-TaskHistory)在执行父实体删除操作时,Hibernate会额外生成一条将子实体外键置空的SQL,破坏了原有关联关系,导致后续无法恢复关联。
执行userRepository.deleteById(id);时生成的SQL中,第一条就是不需要的无效操作:
Hibernate: update task_history set task_id=null where task_id=? and ((is_deleted = false) )
原因分析
当使用**单向@OneToMany + @JoinColumn**配置关联时,Hibernate默认将拥有集合的一方(Task)视为关联的维护端。由于配置了cascade = CascadeType.ALL(包含REMOVE操作),Hibernate在处理级联删除时,会先尝试解除子实体(TaskHistory)与父实体(Task)的关联(即置空外键task_id),再执行子实体的软删除。但软删除仅标记实体为已删除状态,并非物理删除,保留外键是后续恢复关联关系的必要条件。
解决方案
方案1:将单向关联改为双向关联(推荐)
在TaskHistory实体中添加对Task的@ManyToOne关联,同时修改Task中的@OneToMany使用mappedBy指定关联的维护端为TaskHistory:
修改TaskHistory.java:
@Getter @Setter @Entity @Builder @AllArgsConstructor @NoArgsConstructor @Table(name = "task_history") @SQLDelete(sql = "UPDATE task_history SET is_deleted = true WHERE id = ? and version = ? " ) @Where(clause = "is_deleted = false") public class TaskHistory extends AbstractBaseModel { private String state; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "task_id") private Task task; }
修改Task.java中的@OneToMany配置:
@OneToMany(mappedBy = "task", cascade = CascadeType.ALL) private List<TaskHistory> taskHistory;
此时关联的维护端转移到TaskHistory,Hibernate不会再生成置空外键的SQL,而是直接执行软删除操作,同时保留外键关联。
方案2:使用Hibernate注解禁用外键更新
如果不想修改为双向关联,可以在Task的@OneToMany注解上添加@OnDelete(action = OnDeleteAction.NO_ACTION),告诉Hibernate跳过外键关联解除的步骤:
修改Task.java:
@OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "task_id", referencedColumnName = "id") @org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.NO_ACTION) private List<TaskHistory> taskHistory;
验证效果
修改完成后,执行userRepository.deleteById(id);生成的SQL将不再包含置空task_id的语句,仅保留三条软删除操作:
Hibernate: UPDATE task_history SET is_deleted = true WHERE id = ? and version = ? Hibernate: UPDATE task SET is_deleted = true WHERE id = ? and version = ? Hibernate: UPDATE user SET is_deleted = true WHERE id = ? and version = ?
内容的提问来源于stack exchange,提问作者yunus

