本文目录导读:

- 📖 目录导读
- 为什么需要软删除?—— 数据安全与审计的基石
- 环境准备与数据库迁移 —— 开启软删除的第一步
- 模型配置 —— 让Eloquent“记住”删除状态
- 核心API实战 —— 销毁、恢复与永久清除
- 查询范围与排除 —— 如何筛选“活着”和“已死”的数据
- 高级技巧 —— 关联模型软删除、事件监听与清理策略
- 高频问答(FAQ)—— 解决你90%的困惑
📖 目录导读
- 为什么需要软删除?—— 数据安全与审计的基石
- 环境准备与数据库迁移 —— 开启软删除的第一步
- 模型配置 —— 让Eloquent“删除状态
- 核心API实战 —— 销毁、恢复与永久清除
- 查询范围与排除 —— 如何筛选“活着”和“已死”的数据
- 高级技巧 —— 关联模型软删除、事件监听与清理策略
- 高频问答(FAQ)—— 解决你90%的困惑
为什么需要软删除?—— 数据安全与审计的基石
在Web应用开发中,硬删除(物理删除)意味着数据从数据库彻底消失,这在许多业务场景(如订单回收站、用户注销恢复、内容审核)中是灾难性的,Laravel的软删除功能(SoftDeletes)本质上是一个逻辑标记:它并不真正删除记录,而是给数据表添加一个 deleted_at 时间戳字段,当该字段为 NULL 时,代表数据“活着”;当该字段被写入当前时间时,代表数据“已删除”。
这种机制带来了三大核心价值:
- 可恢复性:误操作可以及时回滚。
- 审计追踪:谁在何时删除了数据,有据可查。
- 数据完整性:避免外键约束导致的历史数据断裂。
环境准备与数据库迁移 —— 开启软删除的第一步
我们需要在数据表中增加一个可空的 deleted_at 字段,最优雅的方式是在迁移文件中使用 softDeletes() 方法:
// database/migrations/xxxx_create_posts_table.php
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
// ... 其他字段
$table->timestamps();
$table->softDeletes(); // 关键:添加 deleted_at 字段
});
}
public function down()
{
Schema::dropIfExists('posts');
}
如果你已有数据表,可以创建一个新迁移进行添加:
php artisan make:migration add_soft_deletes_to_posts_table --table=posts
然后在up()方法中使用 $table->softDeletes();,down()中使用 $table->dropSoftDeletes();。
模型配置 —— 让Eloquent“删除状态
这是最核心的一步,在对应的 Eloquent 模型中,引入 SoftDeletes trait:
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes; // 引入软删除特质
protected $dates = ['deleted_at']; // 在 Laravel 7+ 中可选,默认已处理
// 或者使用 $casts
protected $casts = [
'deleted_at' => 'datetime',
];
}
引入这个 trait 后,模型自动拥有了“查询作用域”行为:默认所有查询都会自动过滤掉 deleted_at 不为 NULL 的记录,这意味着你的普通 Post::all() 和 Post::find($id) 永远只返回未删除的数据。
核心API实战 —— 销毁、恢复与永久清除
这是软删除的“三驾马车”操作。
1 软删除(销毁)
调用常规的 delete() 方法即可,但底层变为更新操作:
$post = Post::find(1); $post->delete(); // 实际上执行了 UPDATE posts SET deleted_at = NOW() WHERE id = 1
注意:此操作不会真正移除记录。
2 恢复(Restore)
使用 restore() 方法将 deleted_at 置为 NULL:
$post = Post::withTrashed()->find(1); // 必须先包含已删除的记录 $post->restore();
3 永久清除(Force Delete)
当数据确无保留价值时,使用 forceDelete() 执行物理删除(需先取到包含软删除的记录):
$post = Post::withTrashed()->find(1); $post->forceDelete(); // 真正的 DELETE 语句
查询范围与排除 —— 如何筛选“活着”和“已死”的数据
Laravel 提供了三个极为实用的查询作用域:
| 方法 | 作用描述 |
|---|---|
Model::all() |
常规查询,自动排除软删除数据(默认) |
Model::withTrashed() |
查询结果中包含软删除和未删除的所有记录 |
Model::onlyTrashed() |
只获取已经被软删除的记录(回收站) |
应用场景示例:
- 后台列表:
Post::withTrashed()->orderBy('deleted_at')->get();用于管理员的全局视图。 - 回收站计数:
Post::onlyTrashed()->count();提供用户界面的红点提醒。 - 唯一性校验允许重复,校验时需排除已删除记录:
$exists = Post::withoutTrashed() // 显式声明只查未删除 ->where('slug', $slug) ->exists();
高级技巧 —— 关联模型软删除、事件监听与清理策略
1 关联模型的级联软删除
假如帖子有多个评论,你希望删除帖子时评论也一并软删除,那需要在关联模型上也使用 SoftDeletes trait,并利用模型事件:
// Post.php
protected static function booted()
{
static::deleting(function ($post) {
if ($post->isForceDeleting()) {
// 强制删除时,直接物理删除关联
$post->comments()->forceDelete();
} else {
// 软删除时,级联软删除评论
$post->comments()->delete();
}
});
static::restoring(function ($post) {
// 恢复帖子时,级联恢复评论
$post->comments()->restore();
});
}
2 全局作用域下的关联查询
一定要记住:$post->comments 会自动过滤掉已软删除的评论,除非使用 $post->comments()->withTrashed()->get()。
3 清除过期回收站数据
配合任务调度(php artisan schedule:run),定期清理超过90天的软删除数据:
// 在 Console Kernel 中
$schedule->call(function () {
Post::onlyTrashed()->where('deleted_at', '<', now()->subDays(90))->forceDelete();
})->daily();
高频问答(FAQ)—— 解决你90%的困惑
Q1:软删除会影响数据库索引和查询性能吗?
A1: 会的,但可控。 由于大部分查询都带有 WHERE deleted_at IS NULL,建议为 deleted_at 字段添加索引,复合索引(如 (user_id, deleted_at))能显著提升特定用户的列表查询速度,注意,软删除会增加数据表体积,对于超大流量日志表建议使用分区表或归档。
Q2:开发中我误触发了软删除,马上恢复数据后,自增ID会变吗? A2: 完全不会。 软删除是更新操作,不占用新ID,自增ID仅在硬插入(INSERT)时发生,恢复后,记录数据完整无损,包括关联关系。
Q3:restore() 方法会恢复被软删除的关联模型吗?如果帖子被软删除了,怎么办?
A3: 并不会自动恢复,除非你手动在模型事件中编写了 restored 事件侦听器(如上述第6章节所示),默认情况下,恢复帖子后,评论依然停留在软删除状态,你需要显式调用 $post->comments()->restore(),或者为 Post 模型注册 restored 事件。
Q4:使用 forceDelete() 会不会影响关联数据?
A4: 会! 如果外键约束为 ON DELETE CASCADE,物理删除帖子会级联物理删除评论,如果外键有约束,而你强制删除了父级数据,子级数据可能会因外键约束报错,务必使用上述 isForceDeleting() 监听器来处理关联数据。
Q5:如何判断一条记录是否已被软删除?
A5: 直接检查 $model->trashed() 方法,它返回布尔值:
if ($post->trashed()) {
// 它在回收站
}
Q6:软删除字段可以改名吗?比如用 removed_at?
A6: 可以。 在模型中使用常量 DELETED_AT 覆盖默认名称:
const DELETED_AT = 'removed_at';
同时迁移文件中改为 $table->timestamp('removed_at')->nullable();,但官方默认 deleted_at 约定俗成,不建议随意修改。
Laravel软删除不是复杂的黑魔法,而是数据生命周期管理的标准答案,掌握 [withTrashed、onlyTrashed、restore、forceDelete] 这四板斧,配合事件监听进行级联处理,你的应用数据安全性将提升一个量级,切勿滥用软删除(如日志流水、统计聚合表),动态权衡业务需求,才能让数据层既灵活又干净。