model::beforedelete() 不触发是因为 db::delete() 绕过模型层,仅 model::get()->delete() 或 model::destroy() 等模型实例删除才触发;软删除下 delete() 实为 update,需 withoutsoftdelete() 或 db 类实现硬删。

Model::beforeDelete() 为什么经常不触发
因为 Db::delete() 完全绕过模型,根本不会走事件钩子;只有 Model::get()->delete() 或 Model::destroy() 才会触发 beforeDelete。常见错误是写了 beforeDelete 却在命令行清理任务里用 Db::name('log')->where(...)->delete(),结果函数压根没执行。
触发条件很明确:必须是「模型实例发起的删除」。比如:
-
User::get(123)->delete()✅ 触发 -
User::destroy(123)✅ 触发(但会连带验证、关联、事件) -
Db::name('user')->where('id', 123)->delete()❌ 不触发
如果你真需要拦截逻辑(比如记录谁删了、校验权限),就别用 Db 类,改用模型方式,并确保主键能被正确识别($pk 设置对、没被覆盖)。
Db::delete() 和 Model::destroy() 删除行为差异
两者表面都叫“删除”,底层动作天差地别:
-
Db::delete():直接拼 SQL 执行DELETE FROM ... WHERE ...,无事务包装、无事件、无验证、不查数据、不走关联逻辑——快且可控 -
Model::destroy():先查出记录(可能生成完整模型实例),再逐条调用beforeDelete→ 验证 → 关联处理 →afterDelete→ 真删;1000 条可能生成 1000 次模型 + N 次关联查询,容易 OOM 或锁表
典型翻车场景:UserLog::destroy(['status' => 'expired']) 在日志表有 50 万行时,直接卡死或超时。换成 Db::name('user_log')->where('status', 'expired')->limit(1000)->delete() 就稳得多。
软删除字段为 delete_time 时,delete() 为何返回 0 却不报错
不是失败,是“被软删除机制吞掉了”。只要模型用了 use SoftDelete,所有 ->delete() 调用默认变成 UPDATE,把 delete_time 设为当前时间戳——所以 Db::getRealSql() 看到的是 UPDATE ... SET delete_time = ...,影响行数可能是 1,但业务上你预期的是 DELETE 行为。
要强制物理删除,得显式关掉软删除:
User::withoutSoftDelete()->where('id', 123)->delete();
或者用 Db 类(它完全无视软删除配置)。注意:destroy() 也受软删除影响,同样要加 withoutSoftDelete() 才能硬删。
清理任务中加锁和分页为什么不能省
定时脚本没锁,多实例一跑就是双删;不分页,单次删几万行,MySQL 写锁挂住其他读写,DBA 电话就来了。
- 文件锁最轻量:
file_put_contents(RUNTIME_PATH . 'clean_user_log.lock', time(), LOCK_EX),删完unlink() - 每次最多删 1000 行:
Db::name('user_log')->where('created_at', 'limit(1000)->delete() - 删完立刻检查影响行数:
if ($affected === 0) break;,防条件失效后无限循环 - 别包
transaction:DELETE 本身原子,加事务反而延长锁持有时间
真正麻烦的从来不是“怎么删”,而是“删一半进程崩了怎么办”“删重了会不会丢数据”——这些必须靠锁 + 分片 + 行数判断来兜底,而不是靠模型事件或 try-catch。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











