逻辑删除与唯一约束冲突问题的一个解决方案

本文探讨了在数据库中实现逻辑删除时如何避免违反唯一约束的问题。通过改变删除标记字段的类型并与主键值关联,结合使用联合唯一约束,既满足了逻辑删除的需求,又保持了数据的唯一性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

突然被问有没有写过博客。非常惭愧,没有养成写博客的习惯,但感觉在这行里做了那么些年,有时是该记录点东西了,为了可能的遗忘,或者为网友提供那么一点点的帮助也是好的。

想起正好前两天和几个同事聊了点小问题,其实是个小技巧,就以此为题作点记录,当是练练手吧!

一、问题

现在很多系统都不允许真正删除数据库中的数据,而是通过引入删除标记字段的方式进行假删除,即我们通常说的逻辑删除。其它的数据查询都只查询标记为未删除的数据。目前流行的hibernate 和 mybatis 等框架也提供了对假删除的支持。据我的一些同行说,他们公司甚至不允许在程序中出现删除语句,甚至在实施时,分配给你的数据库用户就不具有删除数据的权限!

但是逻辑删除会面临一个违反唯一约束的问题。比如一个商品表:

create table goods (

    id bigint primary key,

    goods_no varchar(20) not null comment '商品编号',

    goods_name varchar(20) comment '商品名称',

    ......

    deleted bit comment '是否删除', 

    constraint uq_goods_goods_no unique(goods_no)

) ;

当用户添加了一个编码为 '001' 的商品,并且删除之后,goods 表中便有一条goods_no 字段为 '001' 的记录。而如果用户想再添加一个编号为 '001' 的商品,则无法再添加进去了,因为这条数据已经在表中存在了。

但从用户角度看,编号为‘001’ 的这个商品明明都已经删除了;但再次添加的时候,却又说编号已经被占用;好吧,既然已经存在,那我去把它找出来进行修改,或者再删除之后重新添加,却又找不到这条记录!真是莫名其妙!

二、解决

要想解决这种问题,最简单的办法就是在数据库中不要创建唯一约束,数据的唯一性靠应用程序来解决。但可能某种性格的设计师就不乐意了!

笔者认为,可能为了系统运行的性能之类的原因,数据库中可以不用创建各种约束。但在开发阶梯,应当尽量多地在数据为吕创建一些约束。因为约束体现了业务规则,只要违反了约束,肯定会违反业务规则,这样能够把开发过程中可能产生的 bug 尽早地暴露出来,尽早地修复。这样比将来发现问题,再去修改程序,还要修复错误的数据要好得多。其实要坚持在数据库中创建唯一约束,办法也很简单:

1)将删除标记字段类型改为与主键相同的类型,当值为0(主键是数值类型时)或者为空/空字符串(主键是字符串类型)时,表示未删除,当想标记数据为删除状态时,将删除标记字段值修改为与主键相同的值

2)字段的唯一约束,改为字段.与删除标记字段的联合唯一约束

以上述商品表为例,商品表定义改为:

create table goods (

    id bigint primary key,

    goods_no varchar(20) not null comment '商品编号',

    goods_name varchar(20) comment '商品名称',

    ......

    deleted bigint default 0 not null comment '是否删除,当值为0时,表示未删除,当值为非0时,表示已经删除', 

    constraint uq_goods_goods_no unique(goods_no, deleted)

) ;

添加了一个商品,数据库中存在这样一条记录:

{

    "id": 10,

    "goods_no": "001",

    "goods_name": "XXXX牌XXX包装的洗衣粉", 

    "deleted": 0

)

当删除这条记录时,这条记录变成了:

{

    "id": 10,

    "goods_no": "001",

    "goods_name": "XXXX牌XXX包装的洗衣粉", 

    "deleted": 10

)

注意删除字段 deleted = 10,这个字段值取自本条记录的 id 值 

当要再次添加编号为 '001' 的商品时,可以是:

{

    "id": 11,

    "goods_no": "001",

    "goods_name": "XXX牌洋娃娃", 

    "deleted": 0

)

虽然商品编号为 001 的记录已经存在,但这个唯一约束是 goods_no 和 deleted 在一起的,所以不会违反唯一约束。

而如果新加的这条编号为'001'的商品未删除时,想继续添加一条编号为 '001' 的商品,由于新加的记录 deleted = 0,因此会尝试生成两条  goods_no = '001', deleted = 0 的记录,这时违反了唯一约束,添加失败。

var code = "3a62c96a-8d0e-4d5c-a49f-4e7a4de9bdfb"

由此,即满足了逻辑删除的需求,同时又可以不必违反唯一约束。

简单总结一下即是:只要是被标记删除的数据,虽然商品编号可能存在重复,但删除标记字段的值不可能重复(它的值即是主键的值,而主键本身不可能重复);而被标记未被删除的数据,由于其删除标记值都为0,这已经是重复的值了,所以只要商品编号重复,则必然违反唯一约束!

<think>好的,我现在需要解决用户提出的关于MySQL中软删除唯一索引冲突问题。用户提到他们使用软删除,但需要给某个字段添加唯一索引,导致已删除的记录和未删除的记录发生冲突。我需要结合提供的引用内容来整理解决方案。 首先,我要理解问题。当使用软删除时,通常是通过一个字段(比如del_flag)标记记录是否被删除,比如0表示未删除,1表示已删除。如果在某个业务字段(比如name)上设置了唯一索引,那么即使某条记录被软删除(del_flag=1),新插入的同名记录还是会删除的记录冲突,因为唯一索引不考虑del_flag的状态。例如,用户表中已有一条name=张三且del_flag=1的记录,再插入name=张三的新记录时,由于name唯一索引的存在,会导致冲突,因为唯一索引认为这两个name值重复了。 接下来,我需要根据提供的引用内容查找解决方案。引用[3]提到将逻辑删除字段包含在唯一索引中。例如,原来的唯一索引是(name),现在改为(name, del_flag)。但这样如果del_flag是0或1,可能存在多个del_flag=1的同名记录,但用户可能希望已删除的记录可以有重复,而未删除的不允许重复。引用[4]提到将del_flag设置为null,因为唯一索引会忽略null值。比如,未删除时del_flag=0,删除时设置为null。这样唯一索引(name, del_flag)时,已删除的记录的del_flag为null,不会被唯一索引约束,因此允许插入同名的新记录。不过,这样可能需要调整逻辑删除的值,比如0表示未删除,null表示已删除,但需要确保代码中正确处理这种情况。 另外,引用[5]提到了使用REPLACE INTO或者ON DUPLICATE KEY UPDATE来处理冲突,但这可能更多用于处理插入时的覆盖,而不是结构上的解决方案。可能不是用户需要的根本解决办法。 还需要考虑索引的设计。比如,组合索引包含del_flag,并且当del_flag为null时,唯一索引不会将多个null视为重复。例如,在MySQL中,唯一索引允许存在多个null值,所以如果将del_flag设为null,那么即使有多个已删除的同名记录,也不会违反唯一索引。这样,唯一索引可以是(name, del_flag),其中del_flag的非空值(如0)参唯一性约束,而null值不参。这样,未删除的记录必须有唯一的name,而已删除的可以有多个相同name,因为它们的del_flag是null。 此外,引用[3]和[4]都提到了将逻辑删除字段设为null并包含在唯一索引中。这似乎是一个可行的方案。例如,用户表中有name字段和del_flag字段,唯一索引是(name, del_flag)。当记录被删除时,del_flag设为null,这样新插入的name相同的记录在del_flag为0时,唯一索引会检查是否存在(name,0),如果存在则冲突,否则允许插入。而已删除的记录del_flag为null,因此不影响新记录的插入。 但需要注意,有些数据库系统对唯一索引中null值的处理可能不同。比如,MySQL中唯一索引允许有多个null值,而其他数据库如SQL Server则可能不允许。但用户的问题是关于MySQL的,所以可以放心使用这个方法。 另外,还需要调整业务代码,将删除操作改为将del_flag设置为null而不是1。这可能涉及到修改现有的逻辑删除实现,比如MyBatis-Plus中的逻辑删除配置,将删除值设置为null,未删除值设置为0。同时,需要确保应用程序在处理删除时正确更新该字段,并在查询时自动过滤掉del_flag为null的记录。 总结解决方案的步骤: 1. 修改逻辑删除字段的定义,将未删除设为0,已删除设为null。 2. 创建组合唯一索引,包含业务字段(如name)和逻辑删除字段(del_flag)。 3. 调整代码中的删除逻辑,将del_flag设置为null而不是1。 4. 确保查询语句正确排除已删除的记录(del_flag is null)。 这样,当插入新记录时,唯一索引会检查是否存在相同name且del_flag=0的记录,避免重复。而软删除的记录由于del_flag为null,不会参唯一性约束,从而允许插入同名的未删除记录。 还需要验证这个方案是否可行。例如,插入两条name=张三,del_flag=0的记录会导致冲突删除一条后,其del_flag设为null,此时再插入name=张三,del_flag=0的记录是允许的,因为唯一索引中没有该组合。同时,已删除的记录可以有多个name=张三且del_flag为null,因为唯一索引允许null重复。 另外,引用[4]中的例子显示,当del_flag为1时可能仍有冲突,因为组合索引包含del_flag=1的情况。所以将del_flag设为null而非1是关键。这样,在唯一索引中,null被视为不参约束,而0则必须唯一。 最后,可能需要考虑索引的性能问题,组合索引的字段顺序可能会影响查询效率,需要根据实际查询条件来调整索引设计。</think>### MySQL软删除唯一索引冲突解决方案 在使用MySQL时,软删除逻辑删除唯一索引的冲突问题源于:**唯一索引默认包含所有记录(包括已标记为删除的记录)**。例如,若字段`name`被设置为唯一索引,当某条`name=张三`的记录被软删除后(标记为`del_flag=1`),再插入新的`name=张三`的记录会因唯一索引冲突而失败。以下是具体解决方案: --- #### 解决方案一:组合唯一索引 + 逻辑删除字段设为`NULL` 1. **修改逻辑删除字段的定义** - 将逻辑删除值设置为`0`,逻辑删除值设置为`NULL`(而非`1`)[^3][^4]。 - 例如:`del_flag`字段默认值为`0`,删除时更新为`NULL`。 2. **创建组合唯一索引** - 将业务字段(如`name`)和逻辑删除字段(`del_flag`)一起作为唯一索引: ```sql ALTER TABLE user ADD UNIQUE INDEX idx_name_del_flag (name, del_flag); ``` - **原理**:MySQL的唯一索引允许`NULL`值重复,但非`NULL`值必须唯一。因此: - 未删除的记录:`del_flag=0`,组合唯一索引会强制`(name, 0)`唯一。 - 已删除的记录:`del_flag=NULL`,允许存在多个相同`name`的记录。 3. **调整业务代码逻辑** - 查询时自动过滤已删除的记录:`WHERE del_flag = 0`。 - 删除操作时更新`del_flag`为`NULL`而非`1`。 --- #### 解决方案二:逻辑删除字段固定值 + 组合唯一索引 若必须保留逻辑删除字段为固定值(如`1`),则需扩展唯一索引条件: 1. **添加额外字段区分状态** - 新增一个字段`delete_token`(类型为`VARCHAR`),默认值为`''`。 - 删除时,将`delete_token`设置为随机值(如UUID)[^2]。 2. **创建组合索引** ```sql ALTER TABLE user ADD UNIQUE INDEX idx_name_del (name, delete_token); ``` - **原理**:未删除记录的`delete_token`始终为`''`,保证`(name, '')`唯一;已删除记录的`delete_token`为随机值,避免唯一性冲突。 --- #### 方案对比 | 方案 | 优点 | 缺点 | |------|------|------| | **方案一** | 实现简单,直接利用`NULL`特性 | 需修改逻辑删除字段类型(允许`NULL`)| | **方案二** | 兼容原有逻辑删除字段设计 | 需新增字段,增加存储成本 | --- #### 示例代码(MyBatis-Plus配置) ```yaml # 配置逻辑删除字段(方案一) mybatis-plus: global-config: db-config: logic-delete-field: del_flag # 逻辑删除字段名 logic-not-delete-value: 0 # 未删除值 logic-delete-value: null # 已删除值 ``` ---
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值