ThinkPHP5软删除必须模型引入、字段定义、调用方式三者严格对齐:模型需顶部导入use traits\model\SoftDelete;并显式use SoftDelete;,字段须为NULL的DATETIME/TIMESTAMP或BIGINT配$type声明,删除必须用$user->delete()或UserModel::destroy(),禁用Db类直删。

ThinkPHP5 的软删除不是加个字段或改个配置就能跑通,必须模型引入、字段定义、调用方式三者完全对齐,缺一不可;否则 delete() 或 destroy() 会直接物理删库,查不到数据也找不到原因。
模型里怎么正确引入 SoftDelete trait
很多问题出在“看似引入了,其实没生效”。关键点就三个:
-
use traits\model\SoftDelete;必须写在类顶部(注意是traits\model\SoftDelete,不是think\model\concern\SoftDelete—— 后者是 TP6 的路径) - 类定义中必须显式写
use SoftDelete;,光导入命名空间不等于启用 trait - 如果软删字段不是默认的
delete_time(比如叫is_deleted或deleted_at),必须声明protected $deleteTime = 'is_deleted'; -
$defaultSoftDelete在 TP5 中仅当用整型标记(如0/1)时才需配置,且值必须是0;若用时间戳类型,这个属性不要设,否则可能干扰逻辑
数据库字段必须满足哪些约束
这是静默失败最高发的环节:字段类型或 NULL 约束不对,delete() 表面成功,实际没写入时间、也没报错,数据就“消失”了。
- 字段类型建议用
DATETIME或TIMESTAMP,且必须设为NULL;不能带DEFAULT CURRENT_TIMESTAMP—— 否则新记录一插入,delete_time就有值,被误判为已删除 - 若用
BIGINT存时间戳(如delete_time BIGINT UNSIGNED NULL),模型里要加protected $type = ['delete_time' => 'integer'];,否则写入时会被转成字符串再强转为 int,结果常为0 - 绝对禁止设
NOT NULL;哪怕你给它配了默认值0或NULL,MySQL 8.0+ 对约束更敏感,仍可能跳过软删逻辑
为什么 Db::table()->delete() 会直接物理删除
软删除是模型层功能,不是数据库层能力。只要调用了 where(),就已脱离模型上下文,进入 Db 类执行流程。
立即学习“PHP免费学习笔记(深入)”;
-
UserModel::where('id', 1)->delete()是错的——这本质是Db::name('user')->where(...)->delete(),绕过了所有模型逻辑 - 正确做法只有两种:
$user->delete()(实例方法)或UserModel::destroy(1)(静态方法) -
destroy()支持批量:UserModel::destroy([1,2,3])、UserModel::destroy('1,2,3'),也支持闭包条件,但底层仍走模型逻辑 - 想物理删,必须显式传
true:UserModel::destroy(1, true)或$user->delete(true)
查询和恢复软删数据的坑点
默认查询自动过滤掉软删记录,但恢复和查回收站的操作极易写错,且错误不报异常。
- 查全部(含已删):必须用
UserModel::withTrashed()->find(1);withTrashed(true)是旧写法,TP5.1.9+ 已不支持布尔参数 - 只查软删数据:
UserModel::onlyTrashed()->find(1);注意onlyTrashed()返回的是软删状态的记录,不是“被删掉的数据”本身 - 恢复只能用实例方法:
$user->restore();UserModel::where(...)->restore()会报Call to undefined method - 恢复前必须先查出来:
$user = UserModel::onlyTrashed()->find(1); if ($user) $user->restore();,否则restore()静默返回false
最易被忽略的是字段约束与调用路径的耦合性:哪怕模型和字段都对了,只要控制器里写了 Db::name('user')->where(...)->delete(),软删就彻底失效。这不是 bug,是设计使然——ThinkPHP 把软删能力严格绑定在模型实例生命周期内。



















