Hyperf软删除需模型启用SoftDeletes trait、数据库存在DELETED_AT字段且类型匹配、查询链中withTrashed()置于首位三者严格一致,缺一即退化为硬删或查不到数据。

Hyperf 的 SoftDeletes trait 不是开箱即用的“开关”,它依赖模型定义、数据库字段、查询链顺序三者严格匹配,缺一就会退化为硬删或查不到数据。
模型启用 SoftDeletes 前必须确认两件事
Hyperf 会读取模型类里的 DELETED_AT 常量来决定软删除字段名和类型,但不会自动校验数据库是否存在该字段。如果只加 trait 不建字段,delete() 会直接报错:SQLSTATE[42S22]: Column not found: 1054 Unknown column 'deleted_at' in 'where clause'。
- 迁移中必须显式添加字段:
$table->softDeletes()(等价于$table->timestamp('deleted_at')->nullable()) - 若想改字段名(比如用
is_deleted),需在模型里重定义:const DELETED_AT = 'is_deleted';,同时确保该字段是TINYINT(1)或BOOLEAN类型,否则withTrashed()生成的 SQL 条件会失效 - 已有表追加字段,必须新建独立 migration,不能复用旧文件——Hyperf 的模型代码生成器(如
ModelRewriteSoftDeletesVisitor)只在首次生成时注入 trait,不检测字段变更
withTrashed() 查不到软删数据?先看调用位置和作用域
withTrashed() 的本质是临时移除全局作用域里对 deleted_at IS NULL 的过滤,但它只对当前查询链生效,且必须放在最前面。常见错误是写成 UserModel::where('id', 1)->withTrashed()->find(),结果仍返回 null。
-
withTrashed()必须置于链首:UserModel::withTrashed()->where('id', 1)->find() - 检查模型是否重写了
baseQuery()或boot(),手动加了类似$query->whereNull('deleted_at')的逻辑——这会覆盖withTrashed()的效果 - Hyperf 默认不启用全局软删除作用域,需确认是否在模型中显式调用了
use SoftDeletes;并继承了Hyperf\Database\Model\Model
关联查询时 withTrashed() 不生效的解决方式
withTrashed() 默认只作用于主模型,关联模型仍走默认过滤逻辑。例如 OrderModel::withTrashed()->with('user')->find(1) 中,订单能查到已删的,但关联的 user 若已被软删,依然为 null。
- 在关联方法定义里显式调用:
public function user() { return $this->belongsTo(UserModel::class)->withTrashed(); } - 或使用闭包预载:
OrderModel::withTrashed()->with(['user' => function ($q) { $q->withTrashed(); }])->find(1) - 注意:子模型也必须启用
SoftDeletestrait 且数据库有对应字段,否则withTrashed()调用无实际效果
restore() 和 forceDelete() 的行为边界
restore() 不是通用恢复操作,它只对“从软删上下文加载的实例”有效;而 forceDelete() 是彻底物理删除,绕过所有软删逻辑。
- 错误用法:
UserModel::find(1)->restore()——find()默认跳过软删记录,拿到的实例trashed()返回false,调用restore()无效 - 正确做法:
UserModel::onlyTrashed()->find(1)->restore()或UserModel::withTrashed()->find(1)->restore() - 批量恢复不能用
update()直接改字段:UserModel::onlyTrashed()->where('team_id', 5)->update(['deleted_at' => null])会跳过模型事件和类型转换,应使用UserModel::onlyTrashed()->where('team_id', 5)->restore() -
forceDelete()只适用于已加载的模型实例,若要硬删软删状态的数据,得先restore()再delete(true),或直接UserModel::onlyTrashed()->find(1)->forceDelete()
软删除不是“加个 trait 就完事”的功能,它的字段类型、查询链顺序、关联模型独立性,三者任意一个出偏差,都会导致行为不可预期。尤其在 Hyperf 这类强调约定优于配置的框架里,DELETED_AT 常量和迁移字段必须严格一致,否则连报错都可能被静默吞掉。











