直接重写模型 delete() 方法最简单,拦截 DELETE 改为 UPDATE;用 SoftDeleteBehavior 更规范但需注意钩子时机、正确配置及查询时手动过滤已删记录。

直接在模型里重写 delete() 方法最简单
不需要额外 Behavior,只要模型表里有 is_delete 或 deleted_at 字段,就能快速启用软删除。核心是拦截真实 DELETE SQL,改为 UPDATE。
常见错误现象:调用 $model->delete() 后数据库记录真的没了,或者字段没更新、没生效。
- 确认模型类继承自
\yii\db\ActiveRecord - 确保字段名拼写一致,比如
is_delete不能写成is_deleted(除非你主动改了) -
save(false, ['is_delete'])中的false表示跳过验证,避免因其他必填字段为空导致保存失败 - 如果字段是时间类型(如
deleted_at),记得用date('Y-m-d H:i:s')而不是time()
示例:
public function delete()
{
if ($this->hasAttribute('is_delete')) {
$this->is_delete = 1;
return $this->save(false, ['is_delete']);
}
return parent::delete();
}
用 SoftDeleteBehavior 更规范但要注意钩子时机
Behavior 方式适合多模型复用,但必须确认 EVENT_BEFORE_DELETE 确实被触发 —— 这依赖于你调用的是模型实例的 delete(),而不是静态方法如 deleteAll()。
容易踩的坑:
-
deleteByPk()和deleteAll()不会触发 Behavior 的EVENT_BEFORE_DELETE,它们走的是底层 SQL,需单独覆盖或改用updateAll() -
timeAttribute设为true是错的,它应该是一个字符串字段名,比如'deleted_at';设成布尔值会导致$_attribute取不到字段,静默失败 - 行为中
$event->isValid = false必须写,否则仍会执行物理删除
正确配置片段:
public function behaviors()
{
return [
'softDelete' => [
'class' => SoftDeleteBehavior::className(),
'statusAttribute' => 'is_delete',
'deletedValue' => 1,
'activeValue' => 0,
],
];
}
查询时自动过滤已删除记录要用 find() + where()
软删除后,所有“查列表”逻辑都得显式排除已删数据,否则前端会看到脏数据。Yii 不会自动帮你加 WHERE is_delete = 0。
推荐做法是封装一个作用域(scope):
- 在模型中加
public static function find()静态方法,返回带默认条件的ActiveQuery - 避免在每个控制器里重复写
->where(['is_delete' => 0]) - 如果某些场景需要查全部(比如回收站),就用
parent::find()绕过
示例:
public static function find()
{
return parent::find()->where(['is_delete' => 0]);
}
restore() 和 forceDelete() 得自己补全
Behavior 提供了 restore(),但它是实例方法,调用前得先查出模型;而 forceDelete() 是为了绕过软删除强制物理删 —— 这两个都不是开箱即用的,容易漏掉。
关键点:
-
restore()必须在模型已加载的前提下调用,不能对空对象或只含主键的对象直接调用 -
forceDelete()内部用了$this->detach(),但如果 Behavior 是匿名配置(没命名 key),会 detach 失败,建议给 Behavior 起明确名字,如'softDelete' - 批量恢复或强制删,依然得靠
updateAll()/deleteAll(),Behavior 不接管这些
真正要用到恢复功能时,别只依赖 Behavior 自带方法,检查它是否已 attach、字段是否可写、事务是否包裹 —— 这些细节在高并发或复杂业务里很容易出问题。


















