必须同时满足模型启用softdeletes trait、数据库表含deleted_at字段、查询时显式穿透软删除作用域三前提;缺一则delete()退化硬删或restore()静默失败。

要在 Laravel 中安全执行软删除并确保后续能可靠恢复数据,必须同时满足模型启用 SoftDeletes trait、数据库表含 deleted_at 字段、查询时显式穿透软删除作用域这三个前提,缺一不可;直接调用 delete() 或 restore() 而不先查出带 deleted_at 的实例,操作会静默失败或抛出空对象异常。
启用软删除:模型与数据库同步配置
第一步:在数据库迁移中添加 deleted_at 字段。运行命令 php artisan make:migration add_deleted_at_to_posts_table --table=posts,然后在 up() 方法中写 $table->softDeletes(); 这会自动创建一个 nullable timestamp 类型字段,名称固定为 deleted_at,不可更改。
第二步:在对应模型中引入 SoftDeletes trait。打开 app/Models/Post.php,顶部添加 use Illuminate\Database\Eloquent\SoftDeletes;,并在 class 定义内声明 use SoftDeletes;。注意:Laravel 6+ 不再需要手动声明 $dates = ['deleted_at'],Eloquent 会自动识别该字段为日期类型。
第三步:执行迁移 php artisan migrate。这一步不可跳过,【若未执行,delete() 将退化为硬删除,且 restore() 永远无法生效】。验证方式:查数据库 posts 表结构,确认存在 deleted_at 列且允许 NULL。
查询软删除数据:三种场景对应三种方法
默认情况下,User::all()、Post::find(5) 等所有常规查询都会自动忽略 deleted_at IS NOT NULL 的记录——这是全局作用域的强制行为,不是 bug。
方法一:查全部(含已软删)→ 用 withTrashed()
✅ 正确示例:$post = Post::withTrashed()->find(123); 此时即使该 post 已被软删,也能取到完整模型实例,deleted_at 字段值非 null。
方法二:只查已被软删的 → 用 onlyTrashed()
✅ 正确示例:$trashedPosts = Post::onlyTrashed()->where('status', 'draft')->get(); 返回集合中每个模型的 deleted_at 均不为 null。
⚠️ 注意:onlyTrashed()->find(123) 返回 null 并不表示数据丢失,而是说明这条记录根本没被软删过——可能从未调用过 delete(),也可能已被 forceDelete() 彻底清除。
恢复单条软删除记录:必须走实例方法
restore() 是模型实例方法,不能静态调用。它会把 deleted_at 设为 NULL,并触发 restoring/restored 事件。
第一步:通过 withTrashed() 查出目标模型实例
第二步:调用 $model->restore()
✅ 正确写法:
$user = User::withTrashed()->find(456);
if ($user && $user->trashed()) {
$user->restore();
}
❌ 错误写法:
User::find(456)->restore(); // find() 返回 null,报 Call to a member function restore() on null
User::onlyTrashed()->where('email', 'x@y.z')->restore(); // 静态调用,不触发事件,也不走访问器
如需失败时抛异常便于捕获,改用 restoreOrFail() 替代 restore()。
批量恢复与事务保障:防止状态不一致
当恢复操作需联动更新日志、统计或权限表时,必须包裹在 DB::transaction() 中,否则部分成功会导致 deleted_at 清空但日志未写入,形成脏状态。
1、在控制器中 use Illuminate\Support\Facades\DB;
2、获取软删模型实例:$article = Article::withTrashed()->find(789);
3、开启事务并执行恢复与关联操作:
DB::transaction(function () use ($article) {
$article->restore();
AuditLog::create(['action' => 'restore', 'model_type' => 'Article', 'model_id' => $article->id]);
Cache::forget("article_{$article->id}");
});
若任意步骤异常,deleted_at 和日志记录均保持原状,不会出现“恢复了但没记账”的情况。
对于 >500 条的大批量恢复,改用 chunkById() 分片处理,每批独立事务:
Article::onlyTrashed()->where('category_id', 12)->chunkById(100, function ($articles) {
DB::transaction(function () use ($articles) {
$articles->each->restore();
Tag::where('type', 'article')->whereIn('rel_id', $articles->pluck('id'))->update(['is_active' => true]);
});
});











