软删除与唯一索引冲突的本质是唯一约束仍作用于已标记删除的记录,解决关键是让唯一性仅针对未删除数据;推荐使用MySQL 8.0+/PostgreSQL的条件唯一索引,如CREATE UNIQUE INDEX idx_name_unique ON user (name) WHERE is_deleted = 0。

Yii2 中软删除(如用 is_deleted 字段标记)与数据库唯一索引冲突,本质是:唯一索引仍对已“软删”的记录生效,导致新数据插入时因重复值报错。解决关键在于让唯一约束**只作用于有效(未删除)记录**,而非全表。
用条件唯一索引(推荐 MySQL 8.0+/PostgreSQL)
直接在数据库层定义「仅对未删除行生效」的唯一约束,最彻底、性能最优。
- MySQL 8.0+ 示例(假设软删字段为
is_deleted TINYINT(1) DEFAULT 0):
CREATE UNIQUE INDEX idx_name_unique ON user (name) WHERE is_deleted = 0; - PostgreSQL 同理:
CREATE UNIQUE INDEX idx_name_unique ON user (name) WHERE is_deleted = 0; - 注意:需确保
is_deleted字段有明确默认值(如 0),且软删除操作统一设为非 0 值(如 1);索引字段和条件字段都应有适当索引支持查询效率。
应用层绕过校验(兼容旧版本数据库)
当无法使用条件索引(如 MySQL 5.7 或 SQLite),需在 Yii2 模型验证和保存逻辑中主动规避冲突。
- 重写模型的
beforeValidate()或自定义验证规则,在验证唯一性前临时过滤掉已软删记录:
['email', 'unique', 'filter' => ['!=', 'is_deleted', 1]] - 保存前手动检查:
if (self::find()->where(['email' => $this->email, 'is_deleted' => 0])->exists()) { ... } - 务必同步修改所有涉及该字段的查询(如
findOne()、findAll()),默认加->andWhere(['is_deleted' => 0]),避免业务误读软删数据。
迁移与清理策略(防历史数据干扰)
上线软删除后,若原表已有重复值但其中部分已被标记删除,需先清理再建约束,否则条件索引创建会失败。
- 查出违反逻辑的重复项(例如同一
email下存在多个is_deleted = 0的记录):
SELECT email, COUNT(*) FROM user WHERE is_deleted = 0 GROUP BY email HAVING COUNT(*) > 1; - 人工或脚本合并/修正这些脏数据(保留一个有效记录,其余设为
is_deleted = 1或硬删)。 - 确认无冲突后,再执行条件索引创建语句,后续新增数据自然受控。
ActiveRecord 行为封装(提升复用性)
将软删除逻辑和条件查询抽象为 Behavior,避免各模型重复写 andWhere(['is_deleted' => 0])。
- 创建
SoftDeleteBehavior,重写init()注册默认 scope:
public function init() {
parent::init();
$this->owner->on(ActiveRecord::EVENT_BEFORE_FIND, [$this, 'filterSoftDeleted']);
} - 在行为中统一处理 find()、validate()、save() 等环节对
is_deleted的感知,尤其确保unique验证器自动带上软删过滤。 - 配合 Migration 在数据库和代码层保持语义一致,降低维护成本。


















