软删除本质是标记而非删除,需模型启用softdeletes trait且数据库存在匹配类型的deleted_at字段;trashed()是判断软删状态的唯一可靠方法,withtrashed()和onlytrashed()用于显式查询,restore()失败多因字段配置或事务问题。

软删除不是“删了还能找回来”,而是压根没删——只是给记录打了个 deleted_at 时间戳标记。只要模型正确启用 SoftDeletes,且数据库字段存在、类型匹配,后续所有行为才可预期。
怎么确认一条记录是否已被软删除
不能靠肉眼看数据库字段,也不能依赖 find() 是否返回结果——因为默认查询会自动过滤掉软删数据。
-
$user->trashed()是唯一可靠判断方式:返回true表示该模型实例的deleted_at非空 - 必须先查出模型实例(哪怕已软删),再调用该方法。例如:
User::withTrashed()->find(123)->trashed() - 如果直接用
User::find(123),返回null时你根本不知道是“不存在”还是“已被软删” - 注意:
trashed()是实例方法,不能在查询构造器上链式调用(如User::where(...)->trashed()会报错)
查不到软删记录?先看 withTrashed() 和 onlyTrashed() 用对没
这是最常卡住人的地方:默认所有 Eloquent 查询都带 WHERE deleted_at IS NULL 全局作用域。
-
User::all()、User::find(1)、User::where(...)->get()—— 全都不含软删数据 - 要查“全部(含软删)”,必须加
withTrashed():User::withTrashed()->where('email', 'x@y.z')->get() - 要查“仅软删的”,必须加
onlyTrashed():User::onlyTrashed()->where('name', 'test')->first() -
onlyTrashed()->find(1)返回null?不是代码错,而是这条记录根本没被软删过(可能硬删了,或delete()根本没执行成功)
restore() 失败却没报错?重点排查这三处
$user->restore() 看似简单,但失败时往往静默吞掉异常,导致你以为恢复成功了,其实 deleted_at 还在库里。
- 检查数据库字段是否被设为
NOT NULL(比如误加了DEFAULT CURRENT_TIMESTAMP),导致 UPDATE 时无法写入NULL - 检查模型事件钩子:
restoring或restored中是否有逻辑抛出异常或返回false,中断了流程 - 事务中调用后没
commit,或者外层事务被rollback,查库时自然看不到变化 - 批量调用
User::onlyTrashed()->where(...)->restore()不会触发单个模型的restored事件,需手动遍历 + 实例调用
自定义 deleted_at 字段名和类型兼容性
别只改 DELETED_AT 常量就完事,Laravel 对字段类型和解析逻辑很敏感。
- 自定义字段名必须配合
protected $dates = ['my_deleted_at'],否则restore()可能清不掉值 - Laravel 8+ 要求
deleted_at必须是TIMESTAMP NULL,用DATETIME会导致restore()失败且无提示 - 如果在
$casts里写了'deleted_at' => 'date',会覆盖框架内置的时间处理逻辑,破坏trashed()判断 - 迁移中务必用
$table->softDeletes(),而不是手写$table->timestamp('deleted_at')->nullable()(虽等效,但少了一层兼容性保障)
真正容易被忽略的是:软删除生效的前提不是“用了 trait”,而是“trait + 字段存在 + 字段类型正确 + 没被其他配置意外覆盖”。任何一环断开,trashed() 就可能永远返回 false,而你还在奇怪为什么删不掉数据。











