ThinkPHP软删除默认不拦截update操作,需显式添加where('delete_time', null)条件来避免意外修改已删除记录。

软删除记录被 update 意外修改的根源
ThinkPHP 的软删除(SoftDelete)默认只拦截 delete,不拦截 update —— 这是绝大多数人踩坑的起点。你调用 $model->where(...)->update([...]),哪怕该记录已软删除(delete_time 非空),它照样会更新成功。
根本原因:TP 的 update 方法默认不校验软删除状态,也不自动追加 delete_time IS NULL 条件。它只在 select、find 等读操作中默认过滤已删除数据(除非显式调用 withTrashed())。
withTrashed() 不是万能开关,它只影响查询范围
withTrashed() 的作用非常明确:让后续的 select、find、count 等读操作「看到」已软删除的记录。但它对 update、save、destroy 等写操作完全无感 —— 调了也白调。
- ✅
User::withTrashed()->where('id', 1)->find()→ 能查到已删除的用户 - ❌
User::withTrashed()->where('id', 1)->update(['name' => 'xxx'])→ 依然会更新那条已删除记录,withTrashed()在这里毫无作用 - ⚠️ 即使你先
withTrashed()->find()出来再save(),只要没手动判断delete_time,照样覆盖写入
真正安全的更新方式:显式排除已删除记录
最直接、最可控的做法,是在所有可能触及软删除记录的 update 前,主动加上 delete_time IS NULL 条件。别依赖模型层自动处理,自己把关。
推荐写法(适用于 TP6/TP7):
User::where('id', 1)
->where('delete_time', null) // 显式排除已删除
->update(['status' => 2]);
如果你封装了业务逻辑,务必统一检查:
- 用
where('delete_time', null)替代裸where,尤其在管理后台批量操作、API 更新接口中 - 避免使用
save()直接更新实例:先find(),再判断$user->delete_time === null,否则抛异常或跳过 - 注意字段名:TP 默认软删除字段是
delete_time,但如果你改过配置(如设为deleted_at),条件里必须同步改成->where('deleted_at', null)
想更新已删除记录?必须「有意识地绕过」,而非「意外触发」
真有场景需要编辑已软删除的数据(比如恢复前修正信息),那就得明确表达意图,而不是靠模糊的链式调用碰运气。
两种安全做法:
- 用
onlyTrashed()显式限定范围:User::onlyTrashed()->where('id', 1)->update([...])—— 这样语义清晰,且不会误伤正常数据 - 临时关闭软删除行为:
User::withoutGlobalScope(SoftDelete::class)->where('id', 1)->update([...]),但必须确认这是你当前操作的真实需求 - 切记:不要用
withTrashed()+update组合,它既不生效,还制造虚假安全感
软删除不是魔法屏障,它是靠开发者在每次写操作时主动维护的一道逻辑边界。漏掉一次 where('delete_time', null),就可能让回收站里的数据被悄悄改写。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










