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中逻辑删除与唯一性索引的冲突,确保数据的完整性和应用的稳定运行。希望本文能帮助你在遇到类似问题时,能够快速定位并解决。如果你在实践中遇到其他挑战,欢迎在评论区分享你的经验或提问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值