逻辑删除与公共字段填充思路
生产系统很少物理删数据:删了无法审计、难以恢复。业界普遍用"逻辑删除"——加一个标记列,删除只是 UPDATE。同时 create_time、update_time 这类公共字段每张表都有,重复手写很烦。本节讲纯 MyBatis 与 MyBatis-Plus 两种思路。
逻辑删除:纯 MyBatis 怎么做
逻辑删除 = 表加 deleted 标记(0 有效 / 1 删除),删除写成 UPDATE,所有查询都带 deleted = 0。纯 MyBatis 没有内建开关,靠约定与片段复用:
<!-- 删除即更新(也可用 @Update 注解) -->
<!-- UPDATE user SET deleted = 1 WHERE id = #{id} -->
<sql id="notDeleted">
AND deleted = 0
</sql>
<select id="selectById" resultType="User">
SELECT id, name, deleted FROM user
WHERE id = #{id} <include refid="notDeleted"/>
</select>
风险是"每条 SQL 都得记得带条件",容易漏;想全局兜底要写拦截器自动拼条件,工程量大,此时考虑 MyBatis-Plus 更划算。
MyBatis-Plus:@TableLogic
MP 把逻辑删除做成了标准能力:实体字段加注解,删除与查询自动改写:
@TableLogic // 逻辑删除标记字段
private Integer deleted;
此后 deleteById(id) 实际执行 UPDATE user SET deleted=1 WHERE id=?;selectById、selectList 自动追加 deleted=0,无需手写。字段名全局统一时,可用 logic-delete-field: deleted 免注解,值可通过 logic-delete-value / logic-not-delete-value 定制(以官方文档为准)。
公共字段填充思路
create_time / update_time 也分两派:
- 数据库默认值:
DEFAULT CURRENT_TIMESTAMP+ON UPDATE CURRENT_TIMESTAMP(MySQL),简单但灵活度低; - 应用层填充:MyBatis-Plus 提供 MetaObjectHandler,在插入/更新前统一赋值:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime",
LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime",
LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime",
LocalDateTime.class, LocalDateTime.now());
}
}
配合实体字段声明填充时机:
@TableField(fill = FieldFill.INSERT) // 仅插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入与更新都填充
private LocalDateTime updateTime;
纯 MyBatis 没有现成填充钩子,常见做法是 Service 层统一 set,或用拦截器/ThreadLocal 注入当前时间。
两个提醒:逻辑删除列慎建唯一索引——记录被删后同名数据将无法再次插入;时间字段建议"DB 默认值 + 应用填充"双保险,防止漏走框架路径的直写 SQL 产生空值。
逻辑删除解决"删得掉又找得回",公共字段填充解决"每张表都写的样板代码"。小项目手写约定即可,上了 MP 就用 @TableLogic 与 MetaObjectHandler,注意唯一索引与时区两个隐藏坑。