软删除需同时满足模型引入softdeletes trait且数据库表存在可为空的deleted_at字段;否则delete()将执行物理删除或报错。

软删除在 Laravel 中不是默认行为,必须同时满足两个硬性条件:模型引入 SoftDeletes trait 且数据库表存在可为空的 deleted_at 字段;缺一不可,否则 delete() 会直接执行物理删除或报错。
为什么 delete() 还是真删了?检查 trait 和字段是否都到位
最常见的误判是以为加了 trait 就够了,或者只改了迁移但没跑命令。软删除完全失效时,大概率是以下任一情况:
-
deleted_at字段缺失、类型非TIMESTAMP NULL(MySQL)或DATETIME NULL(PostgreSQL/SQLite) - 模型里漏写了
use SoftDeletes;,或use语句写在了class声明之后 - 字段名被手动改成
is_deleted或deleted_time等,而没重写getDeletedAtColumn()方法 - 迁移中用了
$table->softDeletes()->nullable()——softDeletes()本身已隐含nullable(),重复调用可能触发异常
withTrashed() 和 onlyTrashed() 怎么用才不踩坑
这两个方法本质是动态移除或替换全局作用域,不是“开关”式配置。它们只影响当前查询链,且不能混用:
PHP中文网提供Laravel 13.2.0版本下载,Laravel框架 是基于 PHP 8.3+ 的高性能框架,官方推荐通过 Composer 安装。它内置 AI SDK、JSON:API Resources 及原生向量搜索,支持属性驱动开发与队列路由,大幅提升开发效率。相比旧版,13.2.0 优化了缓存 TTL 管理与实时通信,无需 Redis 即可横向扩展。作为现代 Web 开发首选,它兼顾安全与极速体验,助您快速构建企业级应用。
-
User::withTrashed()->where('name', 'John')->get()→ 返回所有匹配记录,含已软删的 -
User::onlyTrashed()->where('name', 'John')->get()→ 只返回deleted_at IS NOT NULL且 name 匹配的记录 -
User::withTrashed()->onlyTrashed()无效,后者会覆盖前者,等价于只调onlyTrashed() - 关联查询(如
$user->posts)默认也受父模型软删状态影响;若要查出用户已软删但文章仍可见,需单独对关系调用withTrashed():$user->posts()->withTrashed()->get()
restore() 不生效?先确认它是不是真被软删过
restore() 只对 deleted_at IS NOT NULL 的记录有效,且不会触发事件(除非显式配置 $dispatchesEvents)。常见失效场景:
- 之前执行过
forceDelete()或原生 SQLDELETE,数据已彻底消失,restore()无从恢复 - 调用
$user->restore()后查不到,是因为默认查询仍过滤掉软删记录;得用User::withTrashed()->find($id)先拿到实例再恢复 - 批量恢复必须带
withTrashed():正确写法是User::withTrashed()->where('deleted_at', '!=', null)->restore();直接User::where(...)->restore()查不到目标,结果为空 -
restore()不会级联更新关联模型的deleted_at,父子软删需自行处理逻辑(比如监听restored事件后手动恢复子记录)
性能与兼容性容易被忽略的点
软删除看似轻量,但在高并发或大数据量场景下,几个细节直接影响稳定性和效率:
-
deleted_at字段必须加索引,尤其当表行数超 10 万后,onlyTrashed()查询会明显变慢;迁移中补加:$table->index('deleted_at'); - 字段设为
NOT NULL或带DEFAULT CURRENT_TIMESTAMP会导致delete()报 SQL 错误——trait 尝试写入NULL,但数据库拒绝 - Laravel 6+ 已自动识别
deleted_at为日期类型,$dates = ['deleted_at']非必需,但保留它可确保 Carbon 实例化,避免时间比较出错 - 唯一索引字段(如
email)在软删后仍占用约束,新用户注册同邮箱会失败;解决方案是改用组合索引:UNIQUE(email, deleted_at),让已删记录的deleted_at非空,从而绕过冲突










