概述
在使用Spring Boot JPA时,执行批量删除操作时,遇到逻辑删除失效的问题。具体而言,当使用deleteAllInBatch
方法时,数据会被物理删除,而不是进行逻辑删除;但是当使用deleteAll
时,逻辑删除操作可以正常生效。经过调查,发现deleteAllInBatch
方法和deleteAll
方法的行为有所不同,导致逻辑删除失败。
症状
- 使用
deleteAllInBatch
方法时,数据直接从数据库中物理删除。 - 使用
deleteAll
方法时,逻辑删除生效,数据并未被物理删除,而是更新了delLogic
字段。
使用的实体类代码
@Entity
public class TemplateField {
@Id
private Long id;
private Integer delLogic; // 用于标记是否被逻辑删除
@PreRemove
public void templateField() {
this.setDelLogic(1); // 设置逻辑删除标记
}
@SQLDelete(sql = "UPDATE t_template_field SET del_logic = 1 WHERE id = ?")
@Where(clause = "del_logic = 0") // 过滤删除标记为0的数据
public void setDelLogic(Integer delLogic) {
this.delLogic = delLogic;
}
}
问题原因
问题的根本原因是deleteAllInBatch
和deleteAll
在执行删除操作时的实现方式不同,导致生命周期回调方法(如@PreRemove
)未被触发。
deleteAllInBatch
和deleteAll
的区别
-
deleteAll()
:该方法会逐个加载实体,并在JPA上下文中处理每个实体的删除操作。每次删除实体时,都会触发实体的生命周期回调方法(如@PreRemove
、@PostRemove
等)。因此,当使用deleteAll()
方法时,你在实体类上定义的逻辑删除(例如通过@PreRemove
标记设置删除标记)可以生效。 -
deleteAllInBatch()
:该方法是一个批量删除操作,通常是直接生成SQL语句一次性删除数据,不会逐个加载实体,因此也不会触发实体的生命周期回调方法。批量操作的优势在于效率较高,但缺点是无法触发与实体相关的生命周期事件,如@PreRemove
和@PostRemove
。
deleteAllInBatch
导致物理删除的原因
deleteAllInBatch()
方法并不会按@PreRemove
中的逻辑设置delLogic
字段,而是直接执行数据库的物理删除操作。这就是为什么在使用deleteAllInBatch()
时,数据会被直接从数据库中删除,而不是进行逻辑删除的原因。
问题解决方案
要解决这个问题,通常有以下几种方式:
方案 1:使用deleteAll()
替代deleteAllInBatch()
如果逻辑删除的需求比性能更为重要,并且不介意性能稍微下降,可以直接使用deleteAll()
方法。这会逐个处理实体,并触发相应的生命周期回调方法,从而确保逻辑删除(即更新delLogic
字段)生效。
方案 2:自定义批量更新方法
如果依然希望使用批量删除操作(如deleteAllInBatch()
),可以自定义一个批量更新的方法,通过直接执行SQL更新操作来实现逻辑删除。这种方式可以保证批量操作时的效率,同时避免物理删除数据。
例如,使用@Modifying
和@Query
注解,执行批量更新操作:
@Modifying
@Query("UPDATE TemplateField tf SET tf.delLogic = 1 WHERE tf.id IN :ids")
int batchLogicalDelete(@Param("ids") List<Long> ids);
该方法会直接更新符合条件的记录,将delLogic
字段设置为1,达到逻辑删除的效果。
方案 3:手动更新实体后再执行批量删除
可以先通过查询获取所有需要“删除”的实体,将它们的delLogic
字段设置为逻辑删除标志,然后再调用deleteAllInBatch()
进行删除操作。
代码示例:
List<TemplateField> fields = templateFieldRepository.findAllById(ids);
fields.forEach(field -> field.setDelLogic(1)); // 更新逻辑删除标志
templateFieldRepository.saveAll(fields); // 保存更新
templateFieldRepository.deleteAllInBatch(fields); // 执行批量删除
这种方式在批量删除前,先手动更新实体,确保逻辑删除字段被正确设置。
问题示例代码
使用deleteAll()
进行逻辑删除
List<TemplateField> fields = templateFieldRepository.findAllById(ids);
fields.forEach(field -> field.setDelLogic(1)); // 更新逻辑删除标志
templateFieldRepository.saveAll(fields); // 保存更新
templateFieldRepository.deleteAll(fields); // 执行逐个删除(触发生命周期方法)
自定义批量更新方法进行逻辑删除
@Modifying
@Query("UPDATE TemplateField tf SET tf.delLogic = 1 WHERE tf.id IN :ids")
int batchLogicalDelete(@Param("ids") List<Long> ids);
// 调用自定义批量逻辑删除方法
templateFieldRepository.batchLogicalDelete(ids);
手动更新实体后再执行批量删除
List<TemplateField> fields = templateFieldRepository.findAllById(ids);
fields.forEach(field -> field.setDelLogic(1)); // 更新逻辑删除标志
templateFieldRepository.saveAll(fields); // 保存更新
templateFieldRepository.deleteAllInBatch(fields); // 执行批量删除
总结
deleteAllInBatch()
方法直接执行SQL批量删除,不会触发实体的生命周期回调方法(如@PreRemove
),导致逻辑删除无效。- 如果需要触发回调方法,可以使用
deleteAll()
,但会影响性能。 - 也可以自定义批量逻辑删除方法,通过直接更新
delLogic
字段来避免物理删除。