1 前言
本文介紹基于mybatis-plus( 版本:3.3.1 ) 的邏輯刪處理辦法,同時考慮到對關(guān)系數(shù)據(jù)庫唯一索引的兼容技矮。
許多公司都要求保留歷史數(shù)據(jù)不能真的徹底刪掉,像我所待過的公司,就要求使用邏輯刪。邏輯刪指的類似添加 is_deleted
字段呵曹,通過 n/y
來標識是否被刪除。邏輯刪的出發(fā)點是好的何暮,但如果被刪除的數(shù)據(jù)還保留在關(guān)系數(shù)據(jù)庫原表時,事情就開始變復雜了铐殃。
關(guān)系數(shù)據(jù)庫中許多表都會添加唯一索引海洼,以確保業(yè)務相關(guān)數(shù)據(jù)的唯一性 ( 由數(shù)據(jù)庫確保業(yè)務數(shù)據(jù)唯一性是最省事簡單的 )。當相同的數(shù)據(jù)被刪除后再次新增富腊,然后再次刪除坏逢,會導致數(shù)據(jù)庫報錯:唯一鍵重復。也就是說赘被,僅靠 is_deleted=n/y
會導致唯一索引不可用是整。
如下示例:
# 表定義
create table user (
id char(64) primary key comment '主鍵',
name varchar(80) not null comment '姓名',
id_card char(60) not null comment '身份證號',
is_deleted char(1) not null comment '邏輯刪標識n/y',
unique index uk_id_card(`id_card`, `is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用戶表';
# 下面這樣的數(shù)據(jù)理應是正常的,但由于唯一索引的存在民假,導致業(yè)務本身受到影響
--------------------------------------
| id | name | id_card | is_deleted |
--------------------------------------
| xx1 | z1 | xxx001 | n | # 有效
--------------------------------------
| xx1 | z1 | xxx001 | y | # 已刪除
--------------------------------------
| xx1 | z1 | xxx001 | y | # 已刪除
唯一索引是必須的浮入,阿里java規(guī)約有這樣的說明:
【強制】業(yè)務上具有唯一特性的字段,即使是多個字段的組合羊异,也必須建成唯一索引事秀。
說明:不要以為唯一索引影響了 insert 速度,這個速度損耗可以忽略野舶,但提高查找速度是明顯的易迹;另外,即使在應用層做了非常完善的校驗控制平道,只要沒有唯一索引睹欲,根據(jù)墨菲定律,必然有臟數(shù)據(jù)產(chǎn)生一屋。
2 兼容邏輯刪和唯一索引的思路
2.1 修改刪除標志的賦值
當數(shù)據(jù)被邏輯刪后窘疮,不再使用 is_deleted = y
,而是使用 is_deleted = {uuid}
陆淀。
# 表定義
create table user (
id char(64) primary key comment '主鍵',
name varchar(80) not null comment '姓名',
id_card char(60) not null comment '身份證號',
is_deleted unsigned tinyint not null comment '邏輯刪標識n/y',
unique index uk_id_card(`id_card`, `is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用戶表';
--------------------------------------
| id | name | id_card | is_deleted |
--------------------------------------
| xx1 | z1 | xxx001 | n | # 有效
--------------------------------------
| xx1 | z1 | xxx001 | {uuid} | # 已刪除
--------------------------------------
| xx1 | z1 | xxx001 | {uuid} | # 已刪除
2.2 添加輔助刪除標志字段
is_deleted = n/y
依然不變考余,另外增加 delete_token = N/A
( 未刪除 )、delete_token = {uuid}
( 已刪除 )轧苫。
# 表定義
create table user (
id char(64) primary key comment '主鍵',
name varchar(80) not null comment '姓名',
id_card char(60) not null comment '身份證號',
is_deleted unsigned tinyint not null comment '邏輯刪標識n/y',
unique index uk_id_card(`id_card`, `is_deleted`, `delete_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用戶表';
-----------------------------------------------------
| id | name | id_card | is_deleted | delete_token |
-----------------------------------------------------
| xx1 | z1 | xxx001 | n | N/A | # 有效
-----------------------------------------------------
| xx1 | z1 | xxx001 | y | {uuid} | # 已刪除
-----------------------------------------------------
| xx1 | z1 | xxx001 | y | {uuid} | # 已刪除
3 邏輯刪分析+處理
此文章基于mybatis-plus 3.3.1進行分析楚堤。mybatis-plus是基于myabtis的增強工具疫蔓。
3.1 mybatis-plus執(zhí)行流程
3.1.1 流程圖
mybatis-plus在springboot中的加載流程
3.1.2 關(guān)鍵流程分析
經(jīng)過上面的流程圖,可以大致了解(mybatis身冬、mybatis-plus)的加載流程衅胀。我們使用基于mybatis-plus的刪除操作時,一般會直接或間接使用 BaseMapper.delete*()
方法酥筝。
mybatis-plus官方文檔的邏輯刪章節(jié)提到:使用Sql注入器注入
LogicDeleteByIdWithFill
并使用(推薦)滚躯。因此我們可以參考該實現(xiàn)進行改寫。
上圖中紅色部分是我們需要進行改造的流程點嘿歌。它的作用是構(gòu)造一個與 xxMapper.method?
對應的 MappedStatement
對象掸掏,存放到 MybatisConfiguration
中,以便后續(xù)由業(yè)務調(diào)用 xxMapper.method?
時使用宙帝。
/**
* mybatis-plus使用的默認sql注入器
*/
public class DefaultSqlInjector extends AbstractSqlInjector {
/**
* 可以看出丧凤,集合中的每個對象,都對應BaseMapper<T>接口的一個方法定義
*/
@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());
}
}
下面介紹mybatis-plus是如何加載和使用Sql注入器:
加載Sql注入器:
public class MybatisPlusAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// ...
// 加載容器中的ISqlInjector步脓,如果不存在愿待,則使用DefaultSqlInjector
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// ...
}
}
使用Sql注入器:
上圖中有介紹調(diào)用流程:MapperFactoryBean.checkDaoConfig() --> configuration.addMapper(Class) --> MybatisMapperRegistry.addMapper() --> MybatisMapperAnnotationBuilder.parse()
public class MybatisMapperAnnotationBuilder extends MapperAnnotationBuilder {
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 嘗試加載xml中對應的sql節(jié)點
loadXmlResource();
// ...
// 獲取xxMapper<T>接口的方法定義
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 如果該方法存在對應的xml sql節(jié)點,則解析節(jié)點靴患,添加MappedStatement
parseStatement(method);
// ...
}
} catch (IncompleteElementException e) {
// ...
}
}
// ########## 關(guān)鍵方法(.inspectInject()) ##########
// 如果xxMapper繼承了Mapper<T>接口仍侥,則使用sql注入器綁定MappedStatement
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
// 獲取MybatisConfiguration中注入的sql注入器,并進行處理
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
}
parsePendingMethods();
}
}
public abstract class AbstractSqlInjector implements ISqlInjector {
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
// ...
// 獲取SQL注入器中包含的方法集合:new Insert()鸳君、new Delete()农渊、...
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
// 構(gòu)建實體對應的table信息,該類有個關(guān)鍵方法相嵌,后續(xù)會用到:TableInfo.getLogicDeleteSql
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// ########## 關(guān)鍵方法(m.inject) ##########
// 循環(huán)注入自定義方法腿时,構(gòu)建MappedStatement,最終添加到MybatisConfiguratiion.mappedStatements
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
// ...
}
}
接下來就是具體的執(zhí)行方法體根據(jù)各自需求來構(gòu)建MappedStatement饭宾,以mybatis-plus的delete方法為例:
public class Delete extends AbstractMethod {
/**
* 注入自定義方法
* PS: 該方法屬于AbstractMethod批糟,放到這里,方便展示
*/
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
// ########## 關(guān)鍵方法 ##########
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
// 重要的常量看铆,標明了delete sql的構(gòu)造格式 "<script>UPDATE %s %s %s %s</script>"
// "<script>UPDATE {tableName} {set語句} {where語句} {注釋}</script>"
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
// 如果使用邏輯刪
if (tableInfo.isLogicDelete()) {
//
sql = String.format(
sqlMethod.getSql(), // 字符串模板
tableInfo.getTableName(), // tableName
sqlLogicSet(tableInfo), // set語句 ####關(guān)鍵部分####
sqlWhereEntityWrapper(true, tableInfo), // where語句
sqlComment()); // 注釋
// 像讀取xml的sql節(jié)點一樣徽鼎,解析該xml文本,構(gòu)造sqlSource
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 構(gòu)造MappedStatement
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
} else {
// ...
}
}
}
參考上述默認的mybatis-plus.Delete對象弹惦,可以了解其構(gòu)造MappedStatement的過程否淤。邏輯刪的關(guān)鍵部分是
sqlLogicSet(tableInfo)
。我們可以通過對其進行改寫棠隐,從而達到兼容唯一索引的目的石抡。
3.2 改造分析
通過章節(jié)3.1,對mybatis-plus的整個加載流程有了大致的了解助泽,也為后續(xù)的改造指明了方向啰扛。BaseMapper.delete*()
方法默認情況下綁定類有:
com.baomidou.mybatisplus.core.injector.methods.Delete
com.baomidou.mybatisplus.core.injector.methods.DeleteByMap
com.baomidou.mybatisplus.core.injector.methods.DeleteById
com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds
這些類的 injectMappedStatement()
都有一個共同的特點嚎京,構(gòu)造sql語句時調(diào)用了 sqlLogicSet(tableInfo)
。我們只需要重構(gòu)該方法的調(diào)用隐解。
3.3 完整代碼
以章節(jié)2中的第二種思路 ( 添加輔助刪除標識字段 ) 為例鞍帝,兩種思路寫法非常類似。
注意事項:該代碼暫不支持基于 UpdateWrapper 等方式的邏輯刪處理煞茫。要么使用UpdateWrapper時手動處理帕涌,要么進一步改寫Sql注入器中的
Update*
。
代碼結(jié)構(gòu)預覽:
|---- config
| |---- MybatisPlusConfiguration.java
| |---- mybatisplus
| |---- BaseEntityFieldsFillHandler.java // 實體類字段適配
| |---- MySqlInjector.java // 自定義sql注入器
| |---- LogicDeleteSqlWrapper.java // 邏輯刪包裝處理類
| |
| |---- method // 自定義sql方法實現(xiàn)
| | |---- Delete.java
| | |---- DeleteByMap.java
| | |---- DeleteById.java
| | |---- DeleteBatchByIds.java
|---- entity
|---- BaseEntity.java
3.3.1 DbAutoConfiguration.java
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.injector.AbstractSqlInjector;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置類
* @author gdzwk
*/
@Slf4j
@Configuration
public class MybatisPlusConfiguration {
/**
* 使用自定義sql注入器
*/
@Bean
public AbstractSqlInjector sqlInjector() {
AbstractSqlInjector sqlInjector = new MySqlInjector();
log.info("db sqlInjector finished.");
return sqlInjector;
}
/**
* BaseEntity 屬性值處理
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new BaseEntityFieldsFillHandler();
}
}
3.3.2 BaseEntity.java
/**
* 實體 基類
* @author gdzwk
*/
@Data
@EqualsAndHashCode
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = -6814276315761594505L;
/**
* 邏輯刪標識
*/
@TableField(value = "is_deleted", fill = FieldFill.INSERT_UPDATE)
@TableLogic(value = "n", delval = "y")
private String deleted;
/**
* PS: 該字段與{@link #deleted}字段配合续徽,方便表在表中加唯一索引
* row1: [..., deleted: 'n', deleteToken: 'N/A']
* row2: [..., deleted: 'y', deleteToken: 'UUID']
* 唯一索引: 業(yè)務字段 + deleted + deleteToken
*/
@TableField(value = "delete_token", fill = FieldFill.INSERT_UPDATE)
private String deleteToken;
// ...其余字段
}
3.3.3 BaseEntityFieldsFillHandler.java
/**
* {@link BaseEntity}字段自適配
* @author gdzwk
*/
public class BaseEntityFieldsFillHandler implements MetaObjectHandler {
private static final String FIELD_DELETED = "deleted";
private static final String FIELD_DELETE_TOKEN = "deleteToken";
// ...
/**
* 插入元對象字段填充(用于插入時對公共字段的填充)
*
* @param metaObject 元對象
*/
@Override
public void insertFill(MetaObject metaObject) {
// 邏輯刪
this.strictInsertFill(metaObject, FIELD_DELETED, String.class, "n");
// 刪除標記
this.strictInsertFill(metaObject, FIELD_DELETE_TOKEN, String.class, "N/A");
// ...
}
/**
* 更新元對象字段填充(用于更新時對公共字段的填充)
*
* @param metaObject 元對象
*/
@Override
public void updateFill(MetaObject metaObject) {
}
}
3.3.4 MySqlInjector.java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.AbstractSqlInjector;
import com.baomidou.mybatisplus.core.injector.methods.*;
import java.util.List;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
/**
* 自定義SQL注入器
* @author gdzwk
*/
public class MySqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new xx.config.mybatisplus.method.Delete(), // 自定義
new xx.config.mybatisplus.method.DeleteByMap(), // 自定義
new xx.config.mybatisplus.method.DeleteById(), // 自定義
new xx.config.mybatisplus.method.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());
}
}
3.3.5 LogicDeleteSqlWrapper.java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
import java.util.List;
/**
* 邏輯刪包裝處理類
* @author gdzwk
*/
public class LogicDeleteSqlWrapper {
// 獲取這些信息蚓曼,便于邏輯刪的處理
private TableInfo tableInfo;
private Class<?> entityType;
private List<TableFieldInfo> fieldList;
private String tableName;
private boolean logicDelete;
public LogicDeleteSqlWrapper(TableInfo tableInfo) {
this.tableInfo = tableInfo;
try {
Field field0 = TableInfo.class.getDeclaredField("entityType");
field0.setAccessible(true);
this.entityType = (Class<?>) field0.get(tableInfo);
Field field1 = TableInfo.class.getDeclaredField("fieldList");
field1.setAccessible(true);
this.fieldList = (List<TableFieldInfo>) field1.get(tableInfo);
Field field2 = TableInfo.class.getDeclaredField("tableName");
field2.setAccessible(true);
this.tableName = (String) field2.get(tableInfo);
Field field3 = TableInfo.class.getDeclaredField("logicDelete");
field3.setAccessible(true);
this.logicDelete = (boolean) field3.get(tableInfo);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("訪問TableInfo.fieldList屬性失敗", e);
}
}
/**
* 來源于{@link AbstractMethod#sqlLogicSet(TableInfo)},
* 并進行改寫以適應項目的邏輯刪需求
*/
public String sqlLogicSet() {
if (BaseEntity.class.isAssignableFrom(entityType)) {
return "SET " + this.getLogicDeleteSql(false, false);
} else {
return "SET " + tableInfo.getLogicDeleteSql(false, false);
}
}
/**
* 給邏輯刪語句片段添加額外的賦值處理
* @param startWithAnd 該sql片段是否需要以AND開頭
* @param isWhere 該sql片段是否為where條件中的語句
* @return 添加額外賦值處理的邏輯刪語句片段
*/
private String getLogicDeleteSql(boolean startWithAnd, boolean isWhere) {
if (logicDelete) {
TableFieldInfo field = fieldList.stream().filter(TableFieldInfo::isLogicDelete).findFirst()
.orElseThrow(() -> ExceptionUtils.mpe("can't find the logicFiled from table {%s}", tableName));
String logicDeleteSql = formatLogicDeleteSql(field, isWhere);
if (startWithAnd) {
logicDeleteSql = " AND " + logicDeleteSql;
}
if (BaseEntity.class.isAssignableFrom(entityType)) {
logicDeleteSql += String.format(", delete_token='%s' ", UUID.randomUUID().toString().replace("-", "").toUpperCase());
}
return logicDeleteSql;
}
return TableInfo.EMPTY;
}
/**
* 來源于{@link TableInfo#formatLogicDeleteSql(TableFieldInfo, boolean)}钦扭,
* 沒有任何變化辟躏,僅為方便調(diào)用
*/
private String formatLogicDeleteSql(TableFieldInfo field, boolean isWhere) {
final String value = isWhere ? field.getLogicNotDeleteValue() : field.getLogicDeleteValue();
if (isWhere) {
if (TableInfo.NULL.equalsIgnoreCase(value)) {
return field.getColumn() + " IS NULL";
} else {
return field.getColumn() + TableInfo.EQUALS + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
}
}
final String targetStr = field.getColumn() + TableInfo.EQUALS;
if (TableInfo.NULL.equalsIgnoreCase(value)) {
return targetStr + TableInfo.NULL;
} else {
return targetStr + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
}
}
}
3.3.6 SQL方法實現(xiàn)
3.3.6.1 Delete.java
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 根據(jù) entity 條件刪除記錄
* PS: 參照{(diào)@link com.baomidou.mybatisplus.core.injector.methods.Delete},并改寫sqlLogic
* @author gdzwk
*/
public class Delete extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
if (tableInfo.isLogicDelete()) {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), new LogicDeleteSqlWrapper(tableInfo).sqlLogicSet(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
} else {
sqlMethod = SqlMethod.DELETE;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
}
}
}
3.3.6.2 DeleteByMap.java
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.util.Map;
/**
* 根據(jù) entity 條件刪除記錄
* PS: 參照{(diào)@link com.baomidou.mybatisplus.core.injector.methods.DeleteByMap},并改寫sqlLogic
* @author gdzwk
*/
public class DeleteByMap extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_MAP;
if (tableInfo.isLogicDelete()) {
sql = String.format(
sqlMethod.getSql(),
tableInfo.getTableName(),
new LogicDeleteSqlWrapper(tableInfo).sqlLogicSet(),
sqlWhereByMap(tableInfo));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Map.class);
return addUpdateMappedStatement(mapperClass, Map.class, getMethod(sqlMethod), sqlSource);
} else {
sqlMethod = SqlMethod.DELETE_BY_MAP;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), this.sqlWhereByMap(tableInfo));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Map.class);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
}
}
}
3.3.6.3 DeleteById.java
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 根據(jù) entity 條件刪除記錄
* PS: 參照{(diào)@link com.baomidou.mybatisplus.core.injector.methods.DeleteById},并改寫sqlLogic
* @author gdzwk
*/
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.isLogicDelete()) {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), new LogicDeleteSqlWrapper(tableInfo).sqlLogicSet(),
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);
}
}
}
3.3.6.4 DeleteBatchByIds.java
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 根據(jù) entity 條件刪除記錄
* PS: 參照{(diào)@link com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds},并改寫sqlLogic
* PS: 雖然批量刪除時,由指定了相同的UUID土全,但就該批次數(shù)據(jù)來說,不會和別的有沖突
* @author gdzwk
*/
public class DeleteBatchByIds extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BATCH_BY_IDS;
if (tableInfo.isLogicDelete()) {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), new LogicDeleteSqlWrapper(tableInfo).sqlLogicSet(),
tableInfo.getKeyColumn(),
SqlScriptUtils.convertForeach("#{item}", COLLECTION, null, "item", COMMA),
tableInfo.getLogicDeleteSql(true, true));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
} else {
sqlMethod = SqlMethod.DELETE_BATCH_BY_IDS;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
SqlScriptUtils.convertForeach("#{item}", COLLECTION, null, "item", COMMA));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
}
}
}
4 邏輯刪后的查詢處理
前面的章節(jié)已完成對兼容唯一索引的邏輯刪分析與實現(xiàn)会涎,但一個完整系統(tǒng)中裹匙,除了邏輯刪除這個動作外,還需要考慮到查詢時如何屏蔽掉被刪除的數(shù)據(jù)末秃。
4.1 闡述疑問
-
對單表的屏蔽:
- 基于mybatis-plus wrapper的單表查詢操作處理還好說點概页,可以自定義改寫,或框架提供相應的處理练慕。
- 對于xml文件中的單表查詢(特別是動態(tài)sql)惰匙,貌似可以直接拼接"and is_deleted = 'n'",但如何確保拼接的sql是絕對沒有語法問題的铃将?
-
對于多表聯(lián)查的屏蔽:
即便能給最外層拼接“and is_deleted = 'n'”项鬼,對于內(nèi)層子查詢,沒過濾被刪數(shù)據(jù)劲阎,會導致查詢量增大
更進一步的處理绘盟?例如分析sql語句,生成AST抽象語法樹悯仙,從而做到對內(nèi)外層查詢均添加"and is_deleted = 'n'"
4.2 分析+處理
項目中進行查詢的方式有很多種龄毡,主要分為使用BaseMapper.select*()
、使用xml文件等方式锡垄,需要逐一確認沦零。
4.2.1 BaseMapper.select*()方法
IService.select*()
、IService.count*()
货岭、IService.lambdaQuery()
路操、IService.page*()
等方法底層均是調(diào)用BaseMapper.select*()
疾渴。
下面就以 com.baomidou.mybatisplus.core.injector.methods.SelectList
類為例進行說明 ( 通過查看源碼,BaseMapper.select*()
均已實現(xiàn)自動屏蔽邏輯刪除的數(shù)據(jù) )寻拂。
public class SelectList extends AbstractMethod {
/**
* ##### 關(guān)鍵方法是super.sqlWhereEntityWrapper() -- where條件語句 #####
*/
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(),
??,
??,
??,
sqlWhereEntityWrapper(true, tableInfo), sqlComment());
// ...
}
/**
* ##### 將父類的方法放到這里程奠,方便查看 #####
* 從下面的條件判斷就可以知道,如果相關(guān)的實體類上有@TableLogic祭钉,就會啟用邏輯刪過濾
*/
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
if (table.isLogicDelete()) {
// ...
} else {
// ...
}
}
}
4.2.2 基于XML的SQL處理
SEELECT SQL寫法可以很復雜瞄沙,為了確保SQL修飾處理的準確性,基本都會先將SQL解析為AST ( 抽象語法樹 )慌核,再進行處理距境。Druid有自己的AST結(jié)構(gòu),Mybatis-Plus則使用了JSqlParser進行SQL解析垮卓。
Mybatis-Plus官網(wǎng)文檔中"多租戶SQL解析器"章節(jié)就是一個使用AST進行SQL改寫的示例垫桂,給了我們很好的啟示。以下是實現(xiàn)代碼:
/**
* @author gdzwk
*/
@Cofiguration
public class DbConfiguration {
/**
* 分頁攔截器
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 添加對SELECT類型語句過濾刪除數(shù)據(jù)的處理
sqlParserList.add(this.logicNotDeleteSqlParser());
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
/**
* select時過濾掉刪除的數(shù)據(jù)
*/
private ISqlParser logicNotDeleteSqlParser() {
TenantSqlParser logicDeleteSqlParser = new TenantSqlParser() {
/**
* 解析處理僅對SELECT類型語句生效
*/
@Override
public boolean doFilter(final MetaObject metaObject, final String sql) {
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
return SqlCommandType.SELECT == mappedStatement.getSqlCommandType();
}
};
logicDeleteSqlParser.setTenantHandler(new TenantHandler() {
@Override
public Expression getTenantId(boolean where) {
return new StringValue("n");
}
@Override
public String getTenantIdColumn() {
return "is_deleted";
}
@Override
public boolean doTableFilter(String tableName) {
return false;
}
});
return logicDeleteSqlParser;
}
}
注意上面代碼的
logicNotDeleteSqlParser()
方法粟按,重寫了TenantSqlParser.doFilter()
方法诬滩,使得該解析器僅對所有SELECT類型的語句生效 ( 可以根據(jù)需求進一步縮小或擴大解析范圍 )。
4.3 注意事項
使用JSqlParser對語句進行修飾改造時灭将,需要注意以下幾點:
考察哪些語句是JSqlParser無法解析的
將SQL解析為抽象語法樹的性能消耗是否會對系統(tǒng)造成影響 ( 因為這里是幾乎所有查詢均會進行解析 )
-
如果某些mapper方法需要查詢被刪除的數(shù)據(jù)疼鸟,可以在
mapper.method??
方法上添加注解@SqlParser(filter = true)
,但需要注意:這會使得mapper.method??
方法上所有的語句解析修飾處理均失效庙曙,例如該查詢方法需要查詢被刪除的數(shù)據(jù) + 多租戶功能空镜。遇到這種矛盾的情況,可以另外添加自定義注解捌朴,參考
MybatisMapperAnnotationBuilder.parse()
方法中的SqlParserHelper.initSqlParserInfoCache(type)
進行改寫即可吴攒。改寫思路:額外的注解用于標識對哪些sql解析器失效。/** * 類上的注解這里使用了methodName砂蔽,可以標記BaseMapper中的某個方法需要進行過濾 * @author gdzwk */ @XxFilter(methodName = "xxxx.xx.UserMapper.selectOne", filter = {...}) @XxFilter(methodName = "xxxx.xx.UserMapper.selectList", filter = {...}) public interface UserMapper extends BaseMapper<User> { /** * 查詢所有的用戶信息洼怔,包括被邏輯刪除的 * @XxFilter注解用于指定該語句不進行哪些SQL解析處理(特定的ISqlParser不會處理該sql) * 在下面的示例中,MySqlParser1察皇、MySqlParser2均不會對該查詢的SQL進行處理 */ @XxFilter(filter = {MySqlParser1.class, MySqlParser2.class}) List<User> listAllUsers(); } // MySqlParser1.class茴厉、MySqlParser2.class均繼承TenantSqlParser,并添加到攔截器中什荣。
-
Mybatis-Plus的SQL解析器的執(zhí)行模式讓我感到困惑的一點是矾缓,假如設置了多個SqlParser,每個SqlParser均會將SQL文本解析為抽象語法樹稻爬,然后才處理嗜闻,最后轉(zhuǎn)換為SQl文本。
這樣的操作非鏈式處理桅锄,會導致重復的"解析為抽象語法樹"動作琉雳,其實完全可以將抽象語法樹對象向下傳遞样眠,鏈式處理完后才返回最終修飾好的SQL文本。
根據(jù)需求可以考慮進行改寫翠肘。
5 總結(jié)
本文討論并給出了基于Mybatis-Plus的邏輯刪處理方案檐束,但邏輯刪本身也不算一個很好的保留歷史無用數(shù)據(jù)的方法,有條件可以從非應用層面進行處理束倍。