forceDelete() 是模型实例方法,用于物理删除软删除记录并触发 deleting/deleted 事件;不支持批量调用,批量删除需用 DB::table() 或原生 SQL;并发安全删除应加 whereNotNull('deleted_at') 条件或使用 lockForUpdate()。

用 forceDelete() 彻底删掉软删除模型
软删除模型默认只是打个标记(deleted_at 非空),不走真实 DELETE SQL。要真正从数据库删掉,必须显式调用 forceDelete() —— 它会跳过软删除逻辑,执行物理删除。
常见错误是直接写 $model->delete(),结果发现记录还在表里,只是 deleted_at 被设了时间;或者误以为 DB::table()->where()->delete() 能绕过,但其实它根本不会触发模型事件、也不会调用 bootSoftDeletes() 相关逻辑,连关联清理都得自己手写。
-
forceDelete()是模型实例方法,必须先查出模型(如User::withTrashed()->find(123)),再调用 - 它会触发
deleting和deleted事件,适合需要清理附件、缓存等场景 - 不支持批量强制删除:没有
where(...)->forceDelete()这种写法,批量只能用原生查询或循环调用
批量物理删除只能靠 DB::table() 或原生 SQL
想一次性删掉几百条软删除记录?别指望 User::onlyTrashed()->forceDelete() —— 这行不通,Laravel 不提供模型作用域的强制删除链式方法。
正确做法是绕过模型,直连数据表。但要注意:这会跳过所有模型生命周期钩子(deleting、deleted)、Eloquent 访问器、以及 SoftDeletes trait 的任何干预。
- 用
DB::table('users')->whereNotNull('deleted_at')->delete()最简洁,也最快 - 如果要保留日志或触发清理逻辑,得先
select id拿到主键列表,再用集合分块调用forceDelete() - MySQL 中
DELETE不释放空间,大量删除后建议后续执行OPTIMIZE TABLE users
恢复已软删除模型前,别漏掉 restore() 的条件检查
很多人在做“强制删之前先确认是否已被软删除”时,直接 if ($user->trashed()) { $user->forceDelete(); },看似合理,但容易忽略并发风险:两次请求之间,另一进程可能已调用 restore(),导致误删活跃数据。
更稳妥的做法是在数据库层面加条件判断,用原子操作确保安全。
- 推荐用
User::where('id', 123)->whereNotNull('deleted_at')->delete(),一行完成「只删已软删除的」 - 如果必须用模型操作,先
lockForUpdate()再查再删:User::withTrashed()->where('id', 123)->lockForUpdate()->first() -
restore()本身不会报错,哪怕模型没软删除过;但它只会清空deleted_at,不会回滚其他字段变更
硬删后关联数据不会自动清理,得自己兜底
用 forceDelete() 或 DB::table() 删主模型时,Laravel 的 onDelete('cascade') 外键约束只对数据库生效,而 Eloquent 的 deleting 事件监听、HasOne/HasMany 的 ->onDelete() 方法(如 cascade)全都不触发。
也就是说:用户被物理删了,它的头像文件、订单记录、日志条目,一个都不会少 —— 除非你提前写了对应逻辑。
- 在模型的
deleting事件里手动删关联(适用于forceDelete()场景) - 数据库建外键时明确加
ON DELETE CASCADE,这样DB::table()删除才真正级联 - 用迁移命令
php artisan schema:dump检查当前外键定义,别假设它存在
最麻烦的不是删不掉,而是删得“太干净”又没留痕:物理删除后,deleted_at 字段消失,连“这条数据曾经存在过”的证据都没了。真要审计或回滚,得靠备份或 binlog。











