软删除与唯一约束共存的解决方案

软删除与唯一约束共存的解决方案

今天一个已经上线了的项目被测出来一个bug,那就是创建了一个用户以后,再把这个用户删了,再创建一个同一个手机号的用户的时候,由于手机号有唯一约束,又用了软删除,就报了手机号码重复的 bug,问题可以简化为:

DROP TABLE IF EXISTS tmp_table;
CREATE TABLE `tmp_table` (
    id bigint primary key auto_increment,
    phone varchar(20) unique not null,
    del_flag int default 0
);

-- 创建一个新用户
INSERT INTO tmp_table (phone) values (15100000000); 

-- 软删除该用户
UPDATE tmp_table SET del_flag = 1 where phone = 15100000000;

-- 创新创建账号
INSERT INTO tmp_table (phone) values (15100000000); 
-- 报错:Duplicate entry '15100000000' for key 'tmp_table.phone'

为了解决这个问题,很容易想到把唯一索引去掉,或者硬删除,但这些都不够优雅。优雅的方式是使用联合约束来进行处理。在有些ORM框架中,软删除是以deleted_at是否为空来做判断的,非空则填上删除时间。这样的话可以在创表的时候创建一个 (phone, deleted_at) 的联合唯一约束,这样就可以解决这个问题了。

DROP TABLE IF EXISTS tmp_table;
CREATE TABLE `tmp_table` (
    id bigint primary key auto_increment,
    phone varchar(20) not null,
    deleted_at timestamp default null
);
ALTER TABLE tmp_tableadd unique key `phone_deleted_at_unique`(`phone`, `deleted_at`);

如果对于一个已经上线了的应用,重新创表不太现实,还是需要进行数据库的迁移,有两种策略,一是替换掉del_flag 软删除改用 deleted_at 软删除的方式,然后添加联合唯一约束,二是再加一个 deleted_at,当要删除一个数据前,先调 update 修改 deleted_at,再删。由于这个项目使用的ORM是Mybatis Plus,目前还没调研怎么替换使用 deleted_at 来进行软删除,所以暂时只是新加了一列 deleted_at 来辅助创建联合唯一约束。数据库迁移步骤如下:

-- 首先删除 phone 的唯一约束
DROP INDEX phone_unique on users;

-- 添加一列 deleted_at
ALTER TABLE tmp_table ADD COLUMN deleted_at timestamp DEFAULT NULL AFTER updated_at;

-- 添加联合唯一约束
ALTER TABLE tmp_table add unique key `users_phone_deleted_at_unique`(`phone`, `deleted_at`);

-- 将已经软删除的数据添加上 deleted_at 的数据
UPDATE tmp_table SET users.deleted_at = users.updated_at where users.del_flag = 1;

这样就可以解决软删除和唯一约束共存时会导致的问题啦,不过这里还留了一个坑待填,那就是 Mybatis plus 怎么用 deleted_at 来进行软删除,这个在后续调研后再写一篇文章。

<think>嗯,用户之前问过List和Map的区别,现在又转向了MySQL表中如何保证人员唯一性。这可能意味着他们正在处理一个涉及数据存储和管理的项目,比如用户管理系统。需要确保每个用户只能被存储一次,避免重复数据。 首先,要确定用户的需求。他们可能在设计数据库表时遇到了重复记录的问题,比如同一用户被多次插入,导致数据冗余或错误。因此,他们需要知道如何通过MySQL的机制来防止这种情况。 接下来,我需要考虑MySQL中保证唯一性的几种方法。主键约束肯定是基础,但用户可能已经知道这一点,或者需要更详细的解释。然后是唯一索引,适用于除了主键之外的其他字段组合需要唯一的情况。比如,用户可能希望邮箱或手机号唯一,这时候唯一索引就派上用场了。 另外,联合唯一索引也是一个重要的点。比如,用户的名字和出生日期的组合必须唯一,这时候就需要在多个字段上建立唯一索引。用户可能没有考虑到这种情况,需要举例说明。 还有,应用层的校验,比如在插入数据前检查是否存在重复记录。不过这种方法在高并发时可能会有问题,比如竞态条件,所以需要强调即使应用层做了检查,数据库层的约束仍然是必要的。 用户可能对触发器的使用不太熟悉,但触发器可以在插入或更新时进检查,虽然可能影响性能,但也是一个可选方案。不过通常建议优先使用约束而不是触发器,因为触发器更复杂且可能更难维护。 UUID或业务主键也是一个考虑点,特别是当使用自增主键不够时,比如需要分布式系统生成唯一ID的情况。这时候用UUID可以保证唯一性,但可能会影响性能,需要权衡。 删除的情况下,如何保持唯一性也是一个问题。比如,用户被标记为删除后,可能允许重新注册,这时候需要将唯一约束删除状态结合,可能用复合索引或者过滤索引,但MySQL不支持过滤索引,可能需要使用时间戳或版本号字段来实现。 最后,要总结最佳实践,强调组合使用多种方法,比如主键+唯一索引+应用层校验,并且根据业务需求调整。用户可能需要具体的例子和代码片段来更好地理解如何实施这些方法。 需要确保回答结构清晰,分点说明,每种方法的适用场景、优缺点,以及示例SQL语句。这样用户可以根据自己的具体情况选择合适的方法。同时,要提醒用户注意性能影响,比如索引过多可能导致写入变慢,需要在唯一性和性能之间找到平衡。</think>### MySQL 保证表中人员唯一性的 7 种核心方法 --- #### **1. 主键约束 (PRIMARY KEY)** - **本质**:强制要求某列或列组合的值**全局唯一且非空** - **适用场景**:人员唯一标识(如用户ID、身份证号) - **实现方式**: ```sql CREATE TABLE persons ( id INT AUTO_INCREMENT PRIMARY KEY, -- 自增主键 name VARCHAR(50), id_card CHAR(18) UNIQUE -- 身份证号唯一 ); ``` - **特点**: - 每张表**只能有一个主键** - 自增主键(`AUTO_INCREMENT`)可避免手动维护唯一性 - 主键默认创建**聚簇索引**,提升查询效率 --- #### **2. 唯一索引 (UNIQUE INDEX)** - **本质**:强制某列或列组合的值**唯一但允许空值** - **适用场景**:邮箱、手机号等业务唯一字段 - **实现方式**: ```sql -- 创建表时定义 CREATE TABLE persons ( id INT PRIMARY KEY, email VARCHAR(100) UNIQUE, phone CHAR(11) UNIQUE ); -- 或后期添加 CREATE UNIQUE INDEX idx_unique_email ON persons(email); ``` - **特点**: - 允许**多个唯一索引**共存 - 空值(NULL)被视为唯一(可存在多个NULL) - 索引提升查询速度,但会增加写操作开销 --- #### **3. 复合唯一约束 (Composite Unique)** - **本质**:多列组合值必须唯一 - **适用场景**:姓名+出生日期、部门+工号等组合唯一 - **实现方式**: ```sql CREATE TABLE employees ( id INT PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), birthdate DATE, department_id INT, UNIQUE KEY (first_name, last_name, birthdate) -- 组合唯一 ); ``` - **特点**: - 单列可重复,但组合值必须唯一 - 比单列唯一约束更灵活 --- #### **4. 应用层校验** - **本质**:在插入/更新数据前先查询是否存在重复 - **示例代码**(Python): ```python def add_user(name, id_card): # 先检查是否存在 existing = execute_query("SELECT id FROM persons WHERE id_card = %s", (id_card,)) if existing: raise Exception("身份证号已存在") # 插入数据 execute("INSERT INTO persons (name, id_card) VALUES (%s, %s)", (name, id_card)) ``` - **注意事项**: - **竞态条件风险**:高并发时可能漏检 - **必须数据库约束配合使用**(最终兜底) --- #### **5. 触发器校验 (Trigger)** - **本质**:在数据库层面进插入/更新前的检查 - **实现方式**: ```sql DELIMITER $$ CREATE TRIGGER check_unique_before_insert BEFORE INSERT ON persons FOR EACH ROW BEGIN IF EXISTS ( SELECT 1 FROM persons WHERE id_card = NEW.id_card ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '身份证号已存在'; END IF; END$$ DELIMITER ; ``` - **优缺点**: - ✅ 数据库层面强制校验 - ❌ 增加性能开销 - ❌ 维护复杂度高 --- #### **6. UUID 或业务主键** - **场景**:分布式系统生成唯一标识 - **实现方式**: ```sql CREATE TABLE users ( uuid CHAR(36) PRIMARY KEY DEFAULT UUID(), name VARCHAR(50) ); ``` - **特点**: - 完全避免主键冲突 - 存储空间大(36字符),索引效率低于整型 --- #### **7. 删除兼容方案** - **问题**:当使用`is_deleted`标记删除时,需保持活跃记录唯一 - **解决方案**: ```sql -- 添加 deleted_at 字段 CREATE TABLE persons ( id INT PRIMARY KEY, email VARCHAR(100), deleted_at DATETIME, UNIQUE KEY (email, deleted_at) -- 仅当 deleted_at 为 NULL 时唯一 ); ``` ```sql -- 查询时过滤已删除数据 SELECT * FROM persons WHERE email = 'user@example.com' AND deleted_at IS NULL; ``` --- ### **最佳实践总结** | **方法** | **适用场景** | **优点** | **缺点** | |-------------------|----------------------------------|--------------------------|--------------------------| | 主键约束 | 唯一标识(ID、身份证) | 强制全局唯一,查询最快 | 只能定义一个 | | 唯一索引 | 邮箱、手机号等业务字段 | 支持多字段,允许NULL | 索引维护成本 | | 复合唯一约束 | 组合字段唯一(姓名+生日) | 灵活控制唯一范围 | 查询需完整匹配 | | 应用层校验 | 所有场景(需配合使用) | 业务逻辑可控 | 无法完全避免并发冲突 | | UUID 主键 | 分布式系统 | 天然去重 | 存储和查询效率较低 | --- ### **防踩坑指南** 1. **NULL 值处理**:唯一索引允许多个`NULL`值,若需禁止重复空值,需加`NOT NULL`约束 ```sql ALTER TABLE persons MODIFY email VARCHAR(100) NOT NULL; CREATE UNIQUE INDEX idx_email ON persons(email); ``` 2. **字符大小写问题**:默认区分大小写,如需不区分: ```sql CREATE UNIQUE INDEX idx_email ON persons(email COLLATE utf8mb4_general_ci); ``` 3. **索引选择性**:超长字段(如地址)不适合建唯一索引,建议用哈希值: ```sql ALTER TABLE persons ADD COLUMN address_hash CHAR(64); CREATE UNIQUE INDEX idx_address_hash ON persons(address_hash); ``` 4. **分区表注意事项**:唯一索引必须包含所有分区键字段 --- 通过组合使用这些方法,可以灵活应对人员唯一性管理的各种业务场景,同时兼顾性能数据完整性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值