laravel软删除需同时满足模型引入softdeletes trait和数据库存在deleted_at字段,否则报错或硬删;查询默认跳过软删记录因全局作用域自动添加where deleted_at is null。

软删除在 Laravel 中不是开箱即用的功能,必须同时满足两个硬性条件:模型引入 SoftDeletes trait,且数据库表中存在 deleted_at 字段;缺一就会导致 delete() 执行硬删,或者直接报错。
模型加 trait 但没加数据库字段会怎样
常见错误现象是执行 $user->delete() 后报错:SQLSTATE[42S22]: Column not found: 1054 Unknown column 'deleted_at' in 'where clause'。这是因为 SoftDeletes trait 内部所有逻辑(包括查询过滤、restore()、forceDelete())都依赖该字段存在。
实操建议:
- 迁移必须显式添加字段,推荐用
$table->softDeletes();(Laravel 9+ 等价于$table->timestamp('deleted_at')->nullable();) - 不要手动写
$table->boolean('is_deleted')或$table->tinyInteger('deleted')—— 类型不匹配会导致withTrashed()查询失效或时间比较异常 - 已有表追加字段,必须新建独立迁移,不能复用旧迁移文件
查询时“看不见”软删数据的真正原因
不是 Eloquent 忘了查,而是它默认启用了全局作用域(Global Scope),自动在所有 SELECT 语句末尾加上 WHERE deleted_at IS NULL。所以 User::all()、User::find(1)、User::where('name', 'foo')->get() 全部跳过软删记录。
实操建议:
-
User::withTrashed()->get():查全部(含deleted_at IS NOT NULL和IS NULL) -
User::onlyTrashed()->get():只查已软删的(deleted_at IS NOT NULL) - 这两个方法只对当前查询链生效,不会污染后续调用 —— 每次需要都得显式加
- API 返回前建议检查:
if ($user->trashed()) { abort(404); },避免把“已删用户”暴露给前端
restore() 失效的典型场景
最常遇到的是:调用 $user->restore() 后数据库里 deleted_at 仍是原值,没变 NULL。根本原因是模型实例没被加载为“软删状态”——比如用 User::find(1) 查出来的永远是未删状态,哪怕数据库里 deleted_at 已有值。
实操建议:
- 恢复前必须从软删上下文中加载模型:
User::onlyTrashed()->find(1)->restore()或User::withTrashed()->find(1)->restore() - 别用
DB::table('users')->where('id', 1)->update(['deleted_at' => null])替代restore()—— 它绕过模型事件、验证和类型转换 - 批量恢复必须走 Eloquent 查询构造器:
User::onlyTrashed()->where('team_id', 5)->restore()
关联模型软删除不联动是设计使然
Laravel 默认不处理父子级联软删。父模型调用 delete(),子模型(如 posts)不会自动软删,也不会自动从 with('posts') 结果中被过滤掉 —— 除非子模型自己也启用了 SoftDeletes,且你在关系定义里显式加了 ->withTrashed()。
实操建议:
- 若需级联软删,必须手动实现,例如在父模型中监听
deleting事件:static::deleting(function ($user) { $user->posts()->update(['deleted_at' => now()]); }); - 外键约束(如
onDelete('cascade'))对软删除完全无效,它只响应硬删触发的DELETE语句 - MySQL 唯一索引对
NULL视为不同值,多个软删记录可能共存于同一唯一键下;PostgreSQL 则相反 —— 这会影响后续restore()是否报Duplicate entry
最容易被忽略的是:软删除字段名 deleted_at 是硬编码在框架里的,不能通过配置改成 is_deleted 或 deleted_time;改它等于重写整个 SoftDeletes trait 的底层逻辑。











