饭宝 发表于 2024-9-23 15:26:48

MybatisPlus逻辑删除与唯一性索引冲突:解决方案与最佳实践

在现代软件开发中,数据持久化层的优化和题目解决是确保应用性能和数据同等性的关键。MybatisPlus,作为Mybatis的加强版本,提供了许多便利功能,包罗逻辑删除。然而,逻辑删除与数据库唯一性索引的结合可能会引发一些预料之外的冲突。本文将深入探讨这一题目,并提供一系列解决方案。
1. 明白逻辑删除与唯一性索引

逻辑删除是一种常见的数据处理方式,它通过设置一个标志字段(如deleted)来标记记录是否已被删除,而不是直接从数据库中物理删除数据。这样可以保留历史数据,方便后续审计或数据分析。
唯一性索引则是数据库中用来确保某一列或某几列组合的值的唯一性,防止重复数据的插入。例如,用户表中通常会为username或email列设置唯一性索引。
2. 冲突场景

当一个被逻辑删除的记录仍然占据其唯一性索引的位置时,新数据尝试插入相同值时就会碰到冲突,导致插入失败。例如,如果一个用户的username被逻辑删除后,另一个新用户尝试使用相同的username注册,就会因为唯一性束缚而失败。
3. 解决方案

3.1 修改唯一性索引

最直接的方法是在数据库层面修改唯一性索引,使其仅在非逻辑删除的记录上见效。例如,在MySQL中,可以创建一个包含username和del_at字段的复合唯一性索引:
ALTER TABLE users ADD COLUMN del_at datetime NULL DEFAULT 0 COMMENT '删除时间';
ALTER TABLE users ADD UNIQUE INDEX unique_username_deleted(username, del_at) USING BTREE;
这样,当添加修改时根据username与del_at字段举行唯一性校验。在逻辑删除时需要给del_at字段举行赋值,该字段仅用来存储删除操作时的时间。
注意:给del_at字段默认值是为了避免MySQL数据库索引字段中如果有null时可能会导致唯一性索引失效的题目。
3.2 新建MybatisPlus逻辑删除拦截器

新建一个LogicSqlInnerInterceptor,继承JsqlParserSupport并实现MybatisPlus的InnerInterceptor,在beforePrepare方法中处理逻辑删除时自动给del_at字段赋值为当前操作时间。具体代码如下:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

/**
* 处理逻辑删除的拦截器,当逻辑删除时给delete_at字段赋值当前时间
* @author haohaiyang
*/
@Component
public class LogicSqlInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    // 日志记录器
    private Logger logger = LoggerFactory.getLogger(LogicSqlInnerInterceptor.class);

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
      // 获取MyBatis的MPStatementHandler对象,用于进一步操作
      PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
      // 获取映射语句对象,用于判断操作类型
      MappedStatement ms = mpSh.mappedStatement();
      // 获取SQL命令类型
      SqlCommandType sct = ms.getSqlCommandType();
      // 判断是否为更新操作
      if (sct == SqlCommandType.UPDATE) {
            // 获取MPBoundSql对象,用于访问和修改SQL语句及参数
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            // 判断是否为逻辑删除操作
            if (ms.getId().contains(".deleteById") || ms.getId().contains(".deleteBatchIds")) {
                try {
                  // 解析SQL语句
                  Statement statement = CCJSqlParserUtil.parse(mpBs.sql());
                  // 获取参数对象
                  Object o = mpBs.parameterObject();
                  // 检查参数对象是否包含逻辑删除字段
                  if (ReflectUtil.hasField(o.getClass(), "delAt")) {
                        // 转换为逻辑删除操作
                        Update update = (Update) statement;
                        String parsedSQL = this.processParser(update, 0, mpBs.sql(), mpBs.parameterObject());
                        mpBs.sql(parsedSQL);
                  }
                } catch (JSQLParserException e) {
                  throw new RuntimeException(e);
                }

            }
      }
    }

    /**
   * 处理更新操作的方法
   * 将更新操作转换为逻辑删除操作
   *
   * @param update 更新语句对象
   * @param index 更新语句中的索引位置
   * @param sql 原始SQL语句
   * @param obj SQL语句的参数对象
   */
    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
      // 添加逻辑删除的设置
      update.addUpdateSet(new UpdateSet(new Column("del_at"), new StringValue(DateUtil.now())));
    }

}
3.3 在MybatisPlusConfig中参加上面创建好的逻辑删除插件

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.ruoyi.framework.interceptor.LogicSqlInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Mybatis Plus 配置
** @author ruoyi
*/
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig
{
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor()
    {
      MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
      // 分页插件
      interceptor.addInnerInterceptor(paginationInnerInterceptor());
      // 乐观锁插件
      interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
      // 阻断插件
      interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
      // 自定义逻辑删除插件
      interceptor.addInnerInterceptor(logicSqlInnerInterceptor());
      return interceptor;
    }

    /**
   * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
   */
    public PaginationInnerInterceptor paginationInnerInterceptor()
    {
      PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
      // 设置数据库类型为mysql
      paginationInnerInterceptor.setDbType(DbType.MYSQL);
      // 设置最大单页限制数量,默认 500 条,-1 不受限制
      paginationInnerInterceptor.setMaxLimit(-1L);
      return paginationInnerInterceptor;
    }

    /**
   * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
   */
    public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor()
    {
      return new OptimisticLockerInnerInterceptor();
    }

    /**
   * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
   */
    public BlockAttackInnerInterceptor blockAttackInnerInterceptor()
    {
      return new BlockAttackInnerInterceptor();
    }

    /**
   * 自定义逻辑删除插件
   */
    public LogicSqlInnerInterceptor logicSqlInnerInterceptor()
    {
      return new LogicSqlInnerInterceptor();
    }
}
4. 实践建议



[*]定期清理逻辑删除的数据:虽然逻辑删除可以保留历史数据,但定期清理不再需要的数据可以开释存储空间并优化索引。
[*]设计时考虑业务需求:在设计数据库和逻辑删除策略时,应充实考虑业务场景,避免不须要的数据冗余。
[*]性能考量:修改唯一性索引或自定义查询条件可能会对数据库性能产生影响,需要根据实际环境权衡。
通过上述方法,我们可以有效解决MybatisPlus中逻辑删除与唯一性索引的冲突,确保数据的完整性和应用的稳定运行。盼望本文能帮助你在碰到类似题目时,可以或许快速定位并解决。如果你在实践中碰到其他挑战,接待在评论区分享你的经验或提问。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: MybatisPlus逻辑删除与唯一性索引冲突:解决方案与最佳实践