Mysql联合唯一索引存在空值时唯一约束失效

问题

因为 gorm 的默认填充有 deleted_at 字段,一开始设置了 open_code 为唯一索引,软删除再插入新数据会出现唯一索引冲突,所以把 open_code + delete_at 创建了联合唯一索引

然而这之后我重复插入(重复注册账号),发现 open_code 并不会出现唯一索引冲突,唯一登录的 open_code 字段重复,正常应该报索引冲突的错误。

背景

表有联合唯一索引如下图:

但是还是可以插入重复的 open_code

其中脱敏的 open_code 都是重复数据

原因

A UNIQUE index creates a constraint such that all values in the index must be distinct. An error occurs if you try to add a new row with a key value that matches an existing row. This constraint does not apply to NULL values except for the BDB storage engine. For other engines, a UNIQUE index allows multiple NULL values for columns that can contain NULL.

唯一约束对NULL值不适用。原因可以这样解释: 比如我们有一个单列的唯一索引,既然实际会有空置的情况,那么这列一定不是NOT NULL的,如果唯一约束对空值也有起作用,就会导致仅有一行数据可以为空,这可能会和实际的业务需求想冲突的,所以通常Mysql的存储引擎的唯一索引对NULL值是不适用的。 这也就倒是联合唯一索引的情况下,只要某一列为空,就不会报唯一索引冲突。

解决方案

方案一

比如在 Java 中,我会为空的列定义一个为空的特殊值来表示 NULL,时间的话可以选择一个历史时间 1997-07-01 00:00:00,数字类型的话可以是 -1。

这个方案需要把唯一性的字段和删除时间加联合所以索引,并且在查询的时候,要带上默认的删除时间条件。

我们在 gorm 里面,默认是 deleted_at 为 NULL,在查找的时候会如下效果

因此还需要做一定的改造, 让查询带上 deleted_at = '默认时间' 的条件(注意:已经删除的可能也需要查询)

方案二

每次删除的时候,给唯一性字段加上时间信息,比如:

  • 这个方案使用很不方便,唯一字段不只 name 的时候软删除要 SET 多个字段

  • 加了时间信息后这个字段的长度一般都会大很多,占用了不必要的空间

  • 为图方便的话例子使用了 concat 函数,数据库操作不建议使用函数。当然也可以先查询出来所有的字段在内存做处理完直接设置值

gorm v1 源码分析

callback 在哪调用

gorm 的很多操作都是通过 callback 来执行的,比如时间字段填充。其中 (*f)(scope) 是执行一个一个 callback 的入口。

查询 SQL delete_at IS NULL 如何拼接

会通过 github.com/jinzhu/gorm.rowQueryCallback 进入 SQL 拼接

关心这里的 scope.CombinedConditionSql(),是产生 Where 条件的,跟进去看:

一眼就看到了,贼尴尬的地方是写死的 %v.%v IS NULL,这里如果不能扩展的话就只能给修改唯一字段了,或者不要默认的软删除了。

删除

查询

全局

更新和创建也类似,这里就不一一列举了。

如何处理

在 gorm 项目的 issue 也有人提到 https://github.com/go-gorm/gorm/issues/3184,作者建议用 Upsert 去解决。

这个特性在 MySQL 官方文档:https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

具体操作看我另一篇文章 https://go.funnycode.cn/rep/gorm/gorm-v2/soft-delete-imporve

Last updated

Was this helpful?