一.问题描述
在业务中经常会有这样一种需求即某字段不能重复,例如用户表的手机又或者是身份证.而遇到这种问题一般两种处理方法,一:插入或修改之前先进行一次查询判断是否存在该记录;二:利用数据库唯一索引约束保证数据的唯一性.
但如果用方法一会有两个缺点,一是低效率,二是在高并发的系统中,很难保证其可靠性,故我们在这使用第二中方法,也就是设置唯一索引.设置唯一索引本身是没问题的,但目前需要基于逻辑删除之上整合.
故要思考如何才能让两者之间完美的整合在一起
二.解决方法
1.历史表
每个表新建一个历史表,存储已经删除的历史数据,缺点是大量的历史表。当然还可以参考mysql schema的table表来设计,存储schema和tableName,然后行数据json类型存储,需要根据场景选择。
2.删除时间(推荐)
删除标志位不使用0、1,改为使用删除时间戳来替代,使用初始值0或者Null来作为未删除标志符,会占用一定的存储空间,但可以显示删除时间,并且 MyBatisPlus 自带就支持了这种做法,使用这个方法只需,将删除标识字段用 datetime 存储,逻辑未删除值和已删除值支持配置为字符串null,另一个值支持配置为函数来获取值如now()
这里贴出 yml 配置文件:
mybatis-plus:
global-config:
db-config:
# 设置逻辑删除值为当前时间
logic-delete-value: "now()"
# 设置未删除值为 "null"
logic-not-delete-value: "null"
3.已删除设为null(推荐)
将未删除标识设置默认值(例如0),再将唯一字段与删除标记添加唯一键约束。当某一记录需要删除时,将删除标记置为NULL。
由于NULL不会和其他字段有组合唯一键的效果,所以当记录被删除时(删除标记被置为NULL时),解除了唯一键的约束。
三.具体实现
这里讲述的是第三种方法,也就是将已删除设为null,这种方法呢 MyBatisPlus 本身是不支持的,故可以利用自定义BaseMapper进行拓展补充,以下是具体实现思路及代码:
1. 继承AbstractMethod组装数据
LogicDeleteById.class
/**
* 根据id逻辑删除
* 情况:唯一主键 未删除:0 删除:null
*
* @author Brave
* @version V1.0
* @date 2021/5/21
*/
public class LogicDeleteById extends AbstractMethod {
/**
* 注入自定义 MappedStatement
*
* @param mapperClass mapper 接口
* @param modelClass mapper 泛型
* @param tableInfo 数据库表反射信息
* @return MappedStatement
*/
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 准备语句
String sql = SqlMethod.UPDATE_BY_ID.getSql();
// set语句
String set = "set deleted_flag = null";
// 组装sql
String buildSql = String.format(
sql,
// 表名
tableInfo.getTableName(),
// set值
set,
// 主键(数据库字段名)
tableInfo.getKeyColumn(),
// 实体类属性名
tableInfo.getKeyProperty(),
// and 主键 = yes | 如果是false的话就是 and 主键 = no
tableInfo.getLogicDeleteSql(true, true));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, buildSql, mapperClass);
return addUpdateMappedStatement(mapperClass, modelClass,"logicDeleteById", sqlSource);
}
}
LogicDeleteByIds.class
/**
* 根据ids逻辑删除
* 情况:唯一主键 未删除:0 删除:null
*
* @author Brave
* @version V1.0
* @date 2021/5/21
*/
public class LogicDeleteByIds extends AbstractMethod {
/**
* 注入自定义 MappedStatement
*
* @param mapperClass mapper 接口
* @param modelClass mapper 泛型
* @param tableInfo 数据库表反射信息
* @return MappedStatement
*/
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// set语句
String set = "set deleted_flag = null";
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BATCH_BY_IDS;
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), set,
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, "logicDeleteByIds", sqlSource);
}
}
这个类主要是将表名&字段&筛选条件&值等内容构建成SqlSource,继而让MyBatisPlus进行处理
2.继承DefaultSqlInjector添加自定义方法
/**
* 自定义 SqlInjector
*
* @author Brave
* @version V1.0
* @date 2021/5/21
*/
@Component
public class MyLogicSqlInjector extends DefaultSqlInjector {
/**
* 如果只需增加方法,保留MP自带方法
* 可以super.getMethodList() 再add
* @return
*/
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new LogicDeleteById());
methodList.add(new LogicDeleteByIds());
return methodList;
}
}
这里主要是将自定义的方法添加到通用方法的集合中
3.创建通用Mapper继承BaseMapper
/**
* 通用Mapper
*
* @author Brave
* @version V1.0
* @date 2021/5/21
*/
public interface SuperMapper<T> extends BaseMapper<T> {
/**
* 唯一主键情况下:根据id进行逻辑删除
*
* @param id
* @return
*/
int logicDeleteById(Serializable id);
/**
* 唯一主键情况下:根据ids进行逻辑删除
*
* @param idList
* @return
*/
int logicDeleteByIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
}
最后业务mapper继承SuperMapper,即可调用自定义方法
四.结尾
自此即可实现基于 MyBatis-Plus 解决数据库逻辑删除与唯一索引问题,我相信这个方法不是最优的,若是老哥们有更好的 idea 欢迎留言,可以一起讨论~