数据库唯一性必须由unique索引保证,仅模型验证无法防止并发重复插入;添加时需在迁移中用$table->unique(),已有重复数据须先清理;软删除场景应使用['email','deleted_at']联合唯一索引。

添加 unique 约束必须用 unique(),不能只靠模型验证
数据库层面的唯一性必须由索引保证,Laravel 模型里的 Rule::unique() 或表单验证只是应用层校验,拦不住并发写入或绕过 API 的直接插入。一旦漏掉数据库索引,email 字段重复插入两次就可能成功——第二次不会报错,但数据已脏。
- 迁移中加索引:在
up()方法里调用$table->string('email')->unique() - 如果字段已存在,先改结构再建索引:
$table->unique('email')单独一行(别带类型声明) - 复合唯一索引写法:
$table->unique(['user_id', 'product_id']) - 注意 MySQL 对
TEXT类型字段建唯一索引需指定长度:$table->text('description')->unique()->fullText()不行,得用$table->string('description', 191)->unique()
已有数据时加 unique 约束失败?先清重或跳过校验
执行 php artisan migrate 报错 SQLSTATE[HY000]: General error: 1062 Duplicate entry 'xxx' for key 'users_email_unique',说明表里已有重复值。MySQL 在建唯一索引时会严格校验全表,不妥协。
- 查重复:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1 - 手动删或合并重复记录(别用
TRUNCATE) - 临时禁用外键和唯一检查(仅开发环境):
DB::statement('SET FOREIGN_KEY_CHECKS=0');+ 清理 + 再启用,但不解决根本问题 - 更稳妥做法:写一个一次性命令清理,再跑迁移,别想着“先 ignore 错误继续”
unique() 和 index() 的区别不只是性能
unique() 本质是带约束的索引,它强制数据库拒绝重复写入;而 index() 只加速查询,不阻止重复。很多人加了 index() 就以为“已经去重了”,结果上线后发现订单号重复生成。
-
$table->string('order_no')->index()→ 查询快,但允许重复插入 -
$table->string('order_no')->unique()→ 插入时报Integrity constraint violation,Laravel 默认转成QueryException - 想同时满足查询快+唯一,不用额外操作:
unique()自带索引效果,无需再写index() - 删除唯一索引用
$table->dropUnique('users_email_unique'),名字默认是{表名}_{字段名}_unique,别手敲错
软删除模型加 unique 约束容易漏掉逻辑删除场景
比如 users 表启用了软删除,email 设了 unique,但用户 A 删除后,用户 B 还能注册同邮箱——因为软删除只是把 deleted_at 设为非空,行还在,唯一索引仍生效,B 插入时就会冲突。
- 常见解法:改成唯一联合索引
['email', 'deleted_at'],让已删除的邮箱不参与新记录的唯一判定 - 迁移写法:
$table->unique(['email', 'deleted_at']) - 注意:这会让
deleted_at IS NULL的记录之间保持 email 唯一,而deleted_at IS NOT NULL的可以重复——符合多数业务预期 - 如果用的是 Laravel 9+,且数据库是 MySQL 8.0+,也可考虑函数索引(如
IFNULL(deleted_at, '0000-00-00')),但迁移文件里不好写,不推荐
php artisan make:migration 自动生成。


















