系列文章
mybatis-plus使用和原理剖析之条件构造器
一、官方说明
逻辑删除
说明:
只对自动注入的 sql 起效:
- 插入: 不作限制
- 查找: 追加 where 条件过滤掉已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 更新: 追加 where 条件防止更新到已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 删除: 转变为 更新
例如:
- 删除:
update user set deleted=1 where id = 1 and deleted=0 - 查找:
select id,name,deleted from user where deleted=0
字段类型支持说明:
- 支持所有数据类型(推荐使用
Integer ,Boolean ,LocalDateTime ) - 如果数据库字段使用
datetime ,逻辑未删除值和已删除值支持配置为字符串null ,另一个值支持配置为函数来获取值如now()
附录:
- 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
- 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。
二、使用方法
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag
logic-delete-value: 1
logic-not-delete-value: 0
@TableField(value = "is_deleted")
@TableLogic(value = "0", delval = "1")
private String isDeleted;
三、原理剖析
官方说明中很重要的一点是逻辑删除"只对自动注入的 sql 起效",也就是说XML中自定义的SQL不会自动拼接逻辑删除条件也不会将物理删除更改为逻辑删除。
为什么不可以像分页插件、多租户插件以及乐观锁插件一样通过SQL拦截器实现哪?问题在于逻辑删除本质是update语句,物理删除是delete语句,没有见过SQL拦截器直接改变DML 语句类型的(我孤陋寡闻也说不定😂)
前面讲了逻辑删除"只对自动注入的 sql 起效",其中“插入”不做限制,“查找”追加逻辑删除谓语片段,“物理删除”改为“逻辑删除”,下面主要看下SelectById 和DeleteById SQL注入方法。
1.SQL注入器原理
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(),
new UpdateById(),
new SelectById(),
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(toList());
}
}
package com.baomidou.mybatisplus.core.enums;
public enum SqlMethod {
INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>"),
UPSERT_ONE("upsert", "Phoenix插入一条数据(选择字段插入)", "<script>\nUPSERT INTO %s %s VALUES %s\n</script>"),
DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "<script>\nDELETE FROM %s WHERE %s=#{%s}\n</script>"),
DELETE_BY_MAP("deleteByMap", "根据columnMap 条件删除记录", "<script>\nDELETE FROM %s %s\n</script>"),
DELETE("delete", "根据 entity 条件删除记录", "<script>\nDELETE FROM %s %s %s\n</script>"),
DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量删除数据", "<script>\nDELETE FROM %s WHERE %s IN (%s)\n</script>"),
LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
LOGIC_DELETE_BY_MAP("deleteByMap", "根据columnMap 条件逻辑删除记录", "<script>\nUPDATE %s %s %s\n</script>"),
LOGIC_DELETE("delete", "根据 entity 条件逻辑删除记录", "<script>\nUPDATE %s %s %s %s\n</script>"),
LOGIC_DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量逻辑删除数据", "<script>\nUPDATE %s %s WHERE %s IN (%s) %s\n</script>"),
UPDATE_BY_ID("updateById", "根据ID 选择修改数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
UPDATE("update", "根据 whereEntity 条件,更新记录", "<script>\nUPDATE %s %s %s %s\n</script>"),
LOGIC_UPDATE_BY_ID("updateById", "根据ID 修改数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"),
SELECT_BY_MAP("selectByMap", "根据columnMap 查询一条数据", "<script>SELECT %s FROM %s %s\n</script>"),
SELECT_BATCH_BY_IDS("selectBatchIds", "根据ID集合,批量查询数据", "<script>SELECT %s FROM %s WHERE %s IN (%s) %s</script>"),
SELECT_ONE("selectOne", "查询满足条件一条数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
SELECT_COUNT("selectCount", "查询满足条件总记录数", "<script>%s SELECT COUNT(%s) FROM %s %s %s\n</script>"),
SELECT_LIST("selectList", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
SELECT_PAGE("selectPage", "查询满足条件所有数据(并翻页)", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
SELECT_MAPS("selectMaps", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
SELECT_MAPS_PAGE("selectMapsPage", "查询满足条件所有数据(并翻页)", "<script>\n %s SELECT %s FROM %s %s %s\n</script>"),
SELECT_OBJS("selectObjs", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s\n</script>");
private final String method;
private final String desc;
private final String sql;
SqlMethod(String method, String desc, String sql) {
this.method = method;
this.desc = desc;
this.sql = sql;
}
public String getMethod() {
return method;
}
public String getDesc() {
return desc;
}
public String getSql() {
return sql;
}
}
=> SqlSessionFactory 若Spring容器总注入ISqlInjector.class 类型Bean则替换默认的DefaultSqlInjector
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory
=> 默认的DefaultSqlInjector 值来源
=> MybatisPlus自动配置类
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
=> 全局默认配置中默认的SQL注入器 private ISqlInjector sqlInjector = new DefaultSqlInjector();
com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils#defaults
=> 默认的SQL注入器 重点 (怎样自定义SQL注入器?)
com.baomidou.mybatisplus.core.injector.DefaultSqlInjector
=> SQL注入器过程
com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
参考我的另一篇文章中的“TableInfo初始化过程分析”部分
mybatis-plus使用和原理剖析之条件构造器
=> AbstractMethod
com.baomidou.mybatisplus.core.injector.AbstractMethod#inject
=> SelectById
com.baomidou.mybatisplus.core.injector.methods.SelectById#injectMappedStatement
2.SelectById
public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true)), Object.class);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
}
SQL字符串模板 SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s")
=> 获取逻辑删除字段的 sql 脚本
com.baomidou.mybatisplus.core.metadata.TableInfo#getLogicDeleteSql
=> 格式化SQL脚本
com.baomidou.mybatisplus.core.metadata.TableInfo#formatLogicDeleteSql
private 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);
}
}
以上代码可以看到,逻辑删除字段支持所有类型,其中字符串类型值使用单引号拼接,其余情况直接用拼接
3.DeleteById
public class DeleteById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
if (tableInfo.isWithLogicDelete()) {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
} else {
sqlMethod = SqlMethod.DELETE_BY_ID;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
tableInfo.getKeyProperty());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
}
}
}
物理删除SQL字符串模板 DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "<script>\nDELETE FROM %s WHERE %s=#{%s}\n</script>")
逻辑删除SQL字符串模板 LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>")
4.总结
Mybatis-Plus TableInfo 是一个极为重要的类,它在项目工程启动时通过com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableInfo(org.apache.ibatis.builder.MapperBuilderAssistant, java.lang.Class<?>) 工具类初始化而来。
四、风险评估
前面我们已经了解到逻辑删除的工作机制,但该实现方式在极端情况下可能会存在SQL注入的风险,造成数据被恶意更改、数据库被删等。
触发条件(很难)
jdbc.url中配置 allowMultiQueries=true
- 逻辑删除值配置在不安全环境中(如使用了配置中心但存在弱密码或者权限控制不当导致属性值被恶意更改)
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDeleted
logic-delete-value: 1
logic-not-delete-value: 0 or 1=1
或
@TableField(value = "is_deleted")
@TableLogic(value = "0 or 1=1", delval = "1")
private Boolean isDeleted;
|