Laravel软删除强制使用deleted_at字段,不可配置为is_deleted等;查询默认排除软删记录,需withTrashed()或onlyTrashed()显式控制;restore()失效多因未从软删状态加载模型;唯一索引在MySQL/PostgreSQL中对NULL处理不同,易致冲突。

软删除字段名必须是 deleted_at
Laravel 的软删除机制硬编码依赖 deleted_at 字段,不是配置项,不能改成 is_deleted 或 deleted_time。模型调用 delete() 时,Eloquent 会自动把当前时间写入这个字段;查询时默认跳过 deleted_at IS NOT NULL 的记录。
实操建议:
- 迁移中必须用
$table->softDeletes()(Laravel 9+ 推荐用$table->timestamp('deleted_at')->nullable()显式声明,避免隐式依赖) - 如果已有表,加字段后需手动执行
php artisan migrate,不能只改模型 - 字段类型必须是
timestamp或datetime,用tinyint模拟会直接失效——whereNull('deleted_at')在非时间类型上行为不可靠
withTrashed() 和 onlyTrashed() 的使用边界
这两个方法只影响「当前查询链」,不改变模型全局行为。常见错误是以为调用一次 withTrashed() 后后续所有查询都包含软删数据——其实不会,每个查询都要显式加。
实操建议:
-
withTrashed():查出所有记录(含已软删),适合回收站列表、审计日志 -
onlyTrashed():只查已软删的,适合清理或恢复操作 - 关联查询时,父模型加了
withTrashed(),子模型默认仍过滤软删数据,需单独在子关系里再加,例如:User::withTrashed()->with(['posts' => function ($q) { $q->withTrashed(); }])
软删除后 restore() 失效的典型原因
最常踩的坑是:调用 $model->restore() 没反应,查数据库发现 deleted_at 还是原值。根本原因通常是模型没被正确加载为“已软删状态”——比如用 find() 查到的是未软删记录,或者用了 withoutGlobalScopes() 绕过了软删除作用域。
实操建议:
- 确保 restore 前模型是从
onlyTrashed()或withTrashed()查询来的,例如:Post::onlyTrashed()->find(123)->restore() - 不要在
restoring或restored事件里修改deleted_at字段,会干扰 Eloquent 内部判断 - 批量恢复要用
Post::onlyTrashed()->where(...)->restore(),别用update(['deleted_at' => null])—— 后者跳过事件和验证
软删除与数据库约束、索引的冲突
MySQL 的唯一索引(UNIQUE)默认把 NULL 视为不同值,所以多个软删记录可能共存于同一唯一键下,导致后续恢复时报 Duplicate entry 错误。PostgreSQL 则默认把多个 NULL 当作相同值处理,行为相反。
实操建议:
- 对含唯一约束的字段(如
email),软删后若允许重复注册/创建,需确认数据库行为是否符合预期 - MySQL 下可考虑加组合唯一索引:
UNIQUE KEY `unique_email_not_deleted` (`email`, `deleted_at`),让软删记录也能参与去重 - 不要在迁移中给
deleted_at加NOT NULL默认值,否则softDeletes()生成的字段会初始化为'0000-00-00 00:00:00',导致软删除逻辑错乱
软删除看着简单,但字段名、查询链作用域、数据库层面的 NULL 处理,三处任何一个没对齐,就会出现“删了查不到、恢复不生效、恢复时报错”这类问题。尤其上线前得确认数据库引擎和唯一索引的实际表现。











