数据库层加联合唯一索引是防重底线,thinkphp的unique规则仅支持单字段校验,多字段组合必须依赖数据库索引兜底;迁移中需显式命名索引如'uk_user_sku'以保障回滚精准,且字段顺序、null约束、数据清洗须与查询逻辑严格对齐。

数据库层加联合唯一索引是防重的底线,ThinkPHP 的 unique 验证规则不能替代它——规则只校验单字段,多字段组合必须靠数据库索引兜底,否则并发写入必然出错。
怎么用 Migration 加带名字的联合唯一索引
在 ThinkPHP 的迁移文件 up() 中,必须显式指定索引名,否则 down() 时无法精准删除:
Schema::table('order_items', function (Blueprint $table) { $table->unique(['user_id', 'sku_id'], 'uk_user_sku'); });- 对应回滚要写
$table->dropUnique('uk_user_sku');,不写名字会默认生成一串哈希,很难维护 - 字段顺序必须和后续 SQL 查询、验证器中
where条件顺序一致,否则索引可能失效(比如先查sku_id再查user_id,但索引是(user_id, sku_id)) - 建表时就加比后期
addUnique()更稳,避免上线后补索引锁表
为什么 unique 规则校验不了联合字段
ThinkPHP 6.x 的 unique 规则底层只拼单字段条件,不支持逗号分隔多字段,例如 ['unique' => ['user', 'username,email']] 这种写法实际只校验了 username 字段,email 被忽略。
- 真正生效的联合校验得靠
Rule::callback()手写逻辑:where(['username' => $data['username'], 'email' => $data['email']])->count() - 更新场景下必须传主键排除自身:
->where('id', '', $id),否则改个地址也会被自己拦住 - 验证器 message 不会自动展开字段名,得手动写
'msg' => '用户名与邮箱组合已存在'
NULL 值让联合索引彻底失效
MySQL 对 NULL 的处理是“多个 NULL 不冲突”,哪怕字段加了联合唯一索引,(1, NULL) 和 (1, NULL) 也能同时插入——这会导致业务上“看似重复却没报错”。
- 导入 Excel 时某列为空,
Db::insert()可能存成NULL,而非空字符串,直接废掉索引 - 解决办法:数据库字段定义加
NOT NULL DEFAULT '',PHP 层入库前用trim()+(string)强转,避免类型歧义(如 Excel 导出的数字123和字符串"123"在 MySQL 中可能不等价) - 查重时也得统一处理:
where(['user_id' => $uid, 'sku_id' => $sid ?: '']),别让NULL混进去
批量导入时怎么避免 N+1 查询压垮数据库
逐行调用 where()->count() 是最常见也最危险的做法——1000 行就是 1000 次查询,IO 直接拉满。
- 正确做法:先把所有待导入的组合键提取出来,拼成字符串数组,如
array_column($rows, 'user_id', 'sku_id')→['1_101', '1_102'] - 用一次
whereRaw("concat(user_id, '_', sku_id) in (".implode(',', $placeholders).")")查出库中已存在的全部键 - 用
array_diff_key()算出干净数据再insertAll(),失败风险可控,且不会因部分重复导致整批回滚 - 注意字符编码:如果
user_id或sku_id含中文或特殊符号,concat可能出错,此时应改用临时表或WHERE (user_id, sku_id) IN ((1,101),(1,102))语法(MySQL 5.7+ 支持)
真正难的不是写那条 ALTER TABLE ... ADD UNIQUE,而是确保 PHP 层查重逻辑、数据库索引定义、字段 NULL 约束、导入数据清洗四者完全对齐——漏掉任意一环,线上就可能出现“查不到重复,却插不进去”的静默失败。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











