文章目录

  • 前言
  • 一、什么是逻辑删除?
  • 二、什么有了逻辑删除还要加唯一索引?
  • 三、如何解决



前言

处理使用 MyBatis-Plus 框架时的,既要使用逻辑删除功能,又要兼容数据库建立唯一索引的问题。
有些啰嗦,想找解决方案直接拉到最后。想了解细节可详细阅读。


一、什么是逻辑删除?

由于数据安全等原因,通常公司都会收回线上环境的 DELETE 权限不允许物理删除数据,所以通常使用逻辑删除,也就是增加一个逻辑删除的字段,用于标识数据已经删除了。

二、什么有了逻辑删除还要加唯一索引?

日常业务中,经常遇到不能重复存在是业务数据(比如:电子发票数据,公司信息数据)。我们为了保证数据的唯一性,就需要给其建立唯一索引。
虽然唯一索引好用,但是结合逻辑删除使用就遇到问题了。假设公司信息数据中,公司名称是唯一索引,那么删除了某个公司的数据,然后再次添加该公司的数据。而由于是逻辑删除只是将删除标记更新,并未真正删除,所以此时插入该公司的数据仍然会被唯一索引认为是重复数据,插入失败。
这个时候有人会说了,将公司名称和逻辑删除标记两个字段建立联合唯一索引不久解决了吗?真的吗?我们来看操作:

  1. 第一次操作:新增公司名称为:金点子无限公司的公司数据

id

公司名称

删除标识[1:正常,0:删除]

1

金点子无限公司

1

  1. 第二次操作:删除该公司数据,再次新增公司名称为:金点子无限公司的公司数据

id

公司名称

删除标识[1:正常,0:删除]

1

金点子无限公司

0

2

金点子无限公司

1

3.第三次操作:再次删除该公司数据,此时发现数据库报唯一索引冲突错误了.因为再次尝试删除,程序尝试将id为2的数据的删除标识更新为0.数据库就将会有两条 公司名称+删除标识一样的数据了

id

公司名称

删除标识[1:正常,0:删除]

1

金点子无限公司

0

2

金点子无限公司

0

由此可见该种方法不能满足日常业务,数据不可能只删除一次,一定会出现多次删除的场景.

三、如何解决

通过上面这个失败的例子,分析发现,第二次删除时是由于删除标识还是0,导致了唯一索引冲突。
那么我们将删除标识动态化不就可以避免了吗?
解决方案如下:

  1. 正常标识还是1,删除标识则更新为该数据的id (推荐)
  2. 正常标识还是1,删除标识则更新为当前时间戳

使用这个策略解决逻辑删除和唯一索引冲突的问题,但是放到 MyBatis-Plus 框架时又遇到问题了。
发现在使用该框架配置逻辑删除的字段值时,貌似只能写死,不能动态指定。这就很头疼了呀。

配置文件的方式:

logic-not-delete-value: 1 # 逻辑未删除值(默认为 1)
      logic-delete-value: 0 # 逻辑已删除值(默认为 0)

注解的方式

@TableLogic(value = "1", delval = "0")
private Long status;

研究阅读了 MyBatis-Plus 实现逻辑删除的源码,最终在给组装sql语句源码中找到了突破点:该段逻辑简单理解就是给要执行的sql拼接上逻辑删除的字段和其对应的值。比如执行一个查询语句:select * from company where name=‘金点子无限公司’ ,转化后就会加上逻辑删除字段:select * from company where name=‘金点子无限公司’ and status=1

// 转化逻辑删除语句相关源码
    private String formatLogicDeleteSql(TableFieldInfo field, boolean isWhere) {
        final String value = isWhere ? field.getLogicNotDeleteValue() : field.getLogicDeleteValue();
        if (isWhere) {
            if (NULL.equalsIgnoreCase(value)) {
                return field.getColumn() + " IS NULL";
            } else {
                return field.getColumn() + EQUALS + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
            }
        }
        final String targetStr = field.getColumn() + EQUALS;
        if (NULL.equalsIgnoreCase(value)) {
            return targetStr + NULL;
        } else {
            return targetStr + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
        }
    }

由”String.format(field.isCharSequence() ? “‘%s’” : “%s”, value);“ 可知,当字段类型为字符类型时,会给参数值拼接两个单引号(where status=‘1’),否则直接使用参数值(where status=1)。这里说的参数值,就是前面我们配置的两个逻辑删除的正常和删除标识。
这个特性我们能用起来了,不拼接单引号,就意味着设置的逻辑删除值中我们可以写sql语句或者支持的sql函数了。这就能解决问题了。废话不多说看操作:

@TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 逻辑删除状态
     * 正常 1
     * 删除 id值
     */
    @TableLogic(value = "1", delval = "id")
    @TableField(select = false)
    private Long status;

@TableLogic 注解的 delval 参数设置为主键字段,status字段的类型设置为非字符串类型。
这样子删除时,组装的sql语句就是这样子了:UPDATE company SET status=id WHERE status=1 AND (name = ‘金点子无限公司’)

使用【删除标识则更新为当前时间戳】方案则是把,delval = “id” 改为 delval = “now()” 即可

注意:数据库字段长度设置,得满足删除标识的最大长度