软删除字段未加$fillable会导致restore()失效;withTrashed()须在查询起始调用;forceDelete()不可逆且触发deleting事件;唯一索引需改为(email, deleted_at)组合索引。

软删除字段没加到 $fillable 会导致 restore() 失效
软删除不是真删,只是把 deleted_at 字段设为当前时间。但如果你在模型里没把 deleted_at 加进 $fillable(或显式排除在 $guarded 外),调用 restore() 时 Laravel 会跳过这个字段更新——结果就是看似执行了恢复,数据库里 deleted_at 依然非空。
实操建议:
- 确认模型中已启用软删除:
use SoftDeletes;并在$casts中声明'deleted_at' => 'datetime' - 检查
$fillable是否包含'deleted_at';若用$guarded = []则无需额外操作 - 测试时别只看返回值,直接查数据库确认
deleted_at是否变回NULL
withTrashed() 和 withoutTrashed() 的调用时机很关键
这两个方法必须在查询构建器最开始就调用,否则会被后续的 where() 或 first() 截断。比如 User::first()->withTrashed() 是无效的——first() 已经触发查询并返回模型实例,再链式调用毫无意义。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
-
User::where('id', 1)->withTrashed()->first()→ 不生效,软删除记录查不到 -
User::withTrashed()->where('id', 1)->first()→ 正确,能查到已软删的记录 - 分页场景下漏掉
withTrashed(),导致->paginate()结果少数据
性能影响:开启 withTrashed() 后 SQL 会去掉 WHERE deleted_at IS NULL 条件,对大表要注意索引是否覆盖 deleted_at 字段。
forceDelete() 真删前最好加二次确认逻辑
forceDelete() 绕过软删除,直接执行 DELETE FROM,不可逆。线上环境没人会裸调它,通常需要业务层兜底。
使用场景:
- 管理员后台「彻底删除」按钮
- 定时任务清理超过 90 天的软删记录
- 迁移脚本中清理测试数据
实操建议:
- 避免在控制器里直接写
$user->forceDelete(),应封装进 Service 方法,并记录操作日志 - 配合数据库外键约束时,注意关联表是否也启用了软删除;否则
forceDelete()可能因外键冲突失败,报错类似Integrity constraint violation - 如果模型有
deleting事件监听器,forceDelete()仍会触发它——这点常被忽略
软删除 + 唯一索引容易引发“重复插入”问题
MySQL 唯一索引默认把 NULL 当作不同值处理。所以当 email 字段加了唯一索引,又启用了软删除,用户 A 软删后,新用户 B 还是可以用相同 email 注册成功——因为两条记录的 deleted_at 分别是 2024-05-01 和 NULL,不冲突。
解决办法只有改索引:
- 用组合唯一索引:
UNIQUE KEY `unique_email_not_deleted` (`email`, `deleted_at`),让已软删的记录和未删的记录在索引层面隔离 - 注意:该索引对
deleted_at IS NULL的查询友好,但对deleted_at IS NOT NULL查询效果一般 - Laravel 迁移中建这种索引要手写
DB::statement(),Schema::create()的unique()方法不支持多列含 NULL 的语义
这个坑在上线后才暴露——测试环境数据量小,软删记录少,冲突概率低,很容易被绕过。



















