软删除需手动注册全局作用域才能生效,否则查询仍返回已删记录;关联查询需单独配置作用域或条件;find()等方法不触发全局作用域,测试时易误判。

软删除字段存在但查询仍返回已删记录
这是最常见的误判:以为加了 SoftDeletes trait 就自动过滤,其实 Laravel 默认所有查询(包括 find()、where()、Eloquent 关联)都包含已软删数据,除非显式排除。
根本原因在于软删除只是给模型加了个 deleted_at 非空判断,并不改变默认查询行为。要真正“只查未删”,必须主动干预查询构建过程。
- 全局作用域是唯一可靠方式——在模型中定义
boot()里用static::addGlobalScope() - 手动调用
withTrashed()或onlyTrashed()是反向操作,不能解决“默认只查未删”需求 - 别在控制器里每个
->get()前都写->whereNull('deleted_at'),既重复又易漏
Laravel 10+ 中启用全局软删除作用域
从 Laravel 9 起,SoftDeletes trait 不再自动注册全局作用域;Laravel 10 更是彻底移除隐式行为。必须手动注册,否则 deleted_at 字段形同虚设。
正确做法是在模型的 boot() 方法中显式添加:
protected static function boot()
{
parent::boot();
static::addGlobalScope(new SoftDeletingScope());
}
- 若跳过这步,即使模型用了
use SoftDeletes;,User::all()依然会拉出所有记录(含deleted_at != null的) - 多个 trait 共存时(如同时用
Uuids),boot()里务必先调parent::boot() - 全局作用域会影响所有静态和实例查询方法,包括
count()、exists(),这点常被忽略
关联查询中软删除失效的典型场景
比如 User::with('posts')->get(),即使 Post 模型启用了软删除并注册了全局作用域,posts 关系仍可能返回已删文章——因为 Eloquent 关系加载走的是独立查询,不受父模型作用域影响。
解决方案不是改父模型,而是确保关联定义本身也受控:
- 在关系方法里显式链式调用
->whereNull('deleted_at'),例如:return $this->hasMany(Post::class)->whereNull('deleted_at'); - 或在关联模型的
boot()中注册全局作用域(推荐),这样所有对该模型的查询(含关联加载)都生效 - 慎用
withTrashed()在关系里,它会绕过全局作用域,导致逻辑不一致
测试时容易漏掉的边界情况
本地开发常靠 tinker 快速验证,但几个关键点一漏就误判功能正常:
- 查单条用
User::find(1)—— 它不走全局作用域,需改用User::where('id', 1)->first()才受控 - 数据库种子(seeder)插入测试数据时,若手动设了
deleted_at,记得清缓存或重启队列监听器,否则模型实例可能读到旧状态 - 使用
DB::table('users')绕过 Eloquent 时,完全不触发软删除逻辑,这不是 bug,是设计使然
软删除不是开关,是需要在每一层查询路径上确认是否生效的约束条件。最危险的错觉,就是看到模型类里写了 use SoftDeletes; 就以为万事大吉。











