接上篇MyBatis-Plus 源码阅读(三)条件构造器原理深度剖析,本文我们讲解一下 MP 中逻辑删除的实现原理。
环境
- 核心依赖:mybatis-plus-spring-boot3-starter 3.5.14
- 基础框架:Spring Boot 3.5.6
- JDK 版本:17
- 开发工具:IntelliJ IDEA
- 数据库:MySQL 8.0.31
前言
在公司开发,为了保证数据的安全性和可追溯性,逻辑删除几乎是一件必做的事,但实现逻辑删除的代价是出现很多重复的 sql 代码片段,这一点通过 MP 的逻辑删除功能能够轻松解决。
简单示例
笔者比较习惯添加一个 delete_timestamp 字段用来表示数据库记录是否删除,使用 bigint 类型。其中 0 表示记录正常,大于 0 表示记录被软删。
建表语句
create table user
(
id int auto_increment
primary key,
name varchar(10) default '' not null,
password varchar(15) default '' not null,
date datetime null,
delete_timestamp bigint default 0 not null comment '用户注销时的时间戳,0 表示用户未注销。',
constraint uniq_name_deleteTimestamp
unique (name, delete_timestamp)
);
用户的 name 字段需要保持唯一,因为添加了逻辑删除字段,所以原来的 name 的唯一键要修改为 name 和 delete_timestamp 的联合唯一键。
添加 MP 配置
mp 提供了两种逻辑删除配置的方式,一种是在 yml 文件添加全局配置,另一种是使用 @TableLogic 注解在单独的实体上配置。
本文采用第一种 yml 全局配置。
在 application.yml 添加下面的配置。
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleteTimestamp # 全局逻辑删除字段名(deleteTimestamp为实体类属性名称)
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
logic-delete-value: UNIX_TIMESTAMP() # 一个 MySQL 内置函数,用来表示当前的时间戳
使用 BaseMapper 提供的方法
现在 mp 就已经替你自动实现逻辑删除的功能了,我们像之前一样使用 BaseMapper 提供的方法。
@Test
public void testLogicDelete() {
int num = userMapper.deleteById(1001);
System.out.println(num);
}
通过控制台的 sql 日志,可以发现 mp 自动帮我们把 sql 改写为下面的形式了。
UPDATE user SET delete_timestamp=UNIX_TIMESTAMP() WHERE id=? AND delete_timestamp=0
源码分析
mp 逻辑删除的源码相对来说比较简单。其实就是给 BaseMapper 的每个方法准备了两套 sql 模板,如果开启了逻辑删除功能,就会采用另一套模板。
public enum SqlMethod {
DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "DELETE FROM %s WHERE %s=#{%s}"),
// ...
LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
}
以 deleteById 方法为例,发现对应两个 SqlMethod 枚举对象。
进入 com.baomidou.mybatisplus.core.injector.methods.DeleteById#injectMappedStatement 方法,里面包含 deleteById 中 SQL 注入的核心逻辑,发现会通过判断 tableInfo 是否开启逻辑删除选择不同的模板。
public class DeleteById extends AbstractMethod {
// ...
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod;
if (tableInfo.isWithLogicDelete()) {
sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream()
.filter(TableFieldInfo::isWithUpdateFill)
.filter(f -> !f.isLogicDelete())
.collect(toList());
if (CollectionUtils.isNotEmpty(fieldInfos)) {
String sqlSet = "SET " + SqlScriptUtils.convertIf(fieldInfos.stream()
.map(i -> i.getSqlSet(EMPTY)).collect(joining(EMPTY)),
"@org.apache.ibatis.reflection.SystemMetaObject@forObject(_parameter).findProperty('" + tableInfo.getKeyProperty() + "', false) != null", true)
+ tableInfo.getLogicDeleteSql(false, false);
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet, tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true));
} else {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true));
}
SqlSource sqlSource = super.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, methodName, sqlSource);
} else {
sqlMethod = SqlMethod.DELETE_BY_ID;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
tableInfo.getKeyProperty());
return this.addDeleteMappedStatement(mapperClass, methodName, super.createSqlSource(configuration, sql, Object.class));
}
}
}
简单 debug 会发现代码走到了第 21 行,即
sql = String.format(sqlMethod.getSql(),
tableInfo.getTableName(),
sqlLogicSet(tableInfo), // 逻辑删除的 set 部分
tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true)); // 逻辑删除的 where 部分
值得关注的是 sqlLogicSet(tableInfo) 和 tableInfo.getLogicDeleteSql(true, true)) 两部分。
前者得到了 SET delete_timestamp=UNIX_TIMESTAMP() 这样的 sql 片段,后者得到了 AND delete_timestamp=0。
往下深入会发现这两个方法都调用了 com.baomidou.mybatisplus.core.metadata.TableInfo#formatLogicDeleteSql 方法
protected String formatLogicDeleteSql(boolean isWhere) {
final String value = isWhere ? logicDeleteFieldInfo.getLogicNotDeleteValue() : logicDeleteFieldInfo.getLogicDeleteValue();
if (isWhere) {
if (NULL.equalsIgnoreCase(value)) {
return logicDeleteFieldInfo.getColumn() + " IS NULL";
} else {
return logicDeleteFieldInfo.getColumn() + EQUALS + String.format(logicDeleteFieldInfo.isCharSequence() ? "'%s'" : "%s", value);
}
}
final String targetStr = logicDeleteFieldInfo.getColumn() + EQUALS;
if (NULL.equalsIgnoreCase(value)) {
return targetStr + NULL;
} else {
return targetStr + String.format(logicDeleteFieldInfo.isCharSequence() ? "'%s'" : "%s", value);
}
}
这个方法的入参 isWhere 的意思是返回的 SQL 片段是否在 where 后面。如果 isWhere 为 true,说明返回的 SQL 片段在 where 后面,那么需要使用逻辑未删除的值(因为我们查找或更新时肯定希望查找到的记录是未被软删的);反之需要使用逻辑删除的值(因为不在 where后面就一定在 set 后面,而 set 说明需要进行软删,所以需要使用逻辑删除的值,即我们配置的 UNIX_TIMESTAMP())
结语
MP 逻辑删除是一个非常简单但实用的功能,仅仅通过多准备一组 sql 模板就解决了让无数程序员头疼的重复 SQL 问题,让人感叹作者的智慧。
如果这篇文章对你理解「逻辑删除原理」有帮助,麻烦点赞 + 关注支持一下~ 你的认可就是我持续输出源码解析系列的最大动力!❤️❤️❤️🤩🤩🤩🚀🚀🚀