TP8.0 不支持 JOIN 删除,必须改用子查询 IN 方式;软删除、字段清空、批量删需严格区分方法,否则易致删错、漏删或静默失败。

ThinkPHP 8.0 删除数据不能直接套用 TP5.1 的写法,尤其是带 JOIN 的 delete(),会报错;软删除、字段值清空、批量删等操作也必须严格区分场景和方法,否则删错、漏删、静默失败都很常见。
Db::delete() 带 JOIN 会报错,必须改用子查询
TP8.0 底层 Query 类已移除对 JOIN + delete() 的支持,像 Db::name('works')->join('order o', ...)->delete('w') 这种写法在 TP8.0 直接抛异常(不是不生效,是语法不通过)。官方推荐且唯一可靠的方式是用子查询构造 IN 条件:
- 先查出要删的主表 ID:用闭包传入子查询,
where('order_num', 'IN', function($query) use($timeLimit) { $query->name('order')->where('status', 0)->where('created_at', 'field('order_num'); }) - 再执行主表 delete():
Db::name('works')->where(...)->delete(),注意这里不带别名参数 - 子查询里不要用
select()或find(),只用field(),否则返回结果类型不匹配导致 SQL 错误 - 如果子查询结果为空,整个 delete() 会变成
WHERE order_num IN (),MySQL 会返回 0 行影响,但不会报错——需提前判断子查询是否真有数据
软删除必须模型调用,Db 类直删 = 物理删除
用 Db::name('user')->where('id', 1)->delete() 或 Db::table('user')->delete(1),哪怕模型里定义了 SoftDelete trait,也会跳过所有软删逻辑,直接执行 DELETE FROM。真正启用软删只有一条路:
- 模型类顶部引入
use think\model\concern\SoftDelete;,并显式声明use SoftDelete; - 数据库字段如
delete_time必须设为DATETIME NULL,禁用DEFAULT CURRENT_TIMESTAMP,否则新记录一插入该字段就有值,后续查不到 - 删除只能走模型实例:
$user->delete()或UserModel::destroy(1),不能传 Db 对象进去 - 恢复必须先查出已软删记录:
UserModel::onlyTrashed()->find(1),再调$user->restore();UserModel::where(...)->restore()会报方法不存在
想清空字段值?别用 delete(),那是删整行
搜“删除字段”却调 delete() 或 destroy(),结果把整条用户记录干掉了——这是高频误操作。清空某个字段(比如 temp_data、cache_json)本质是 UPDATE 操作:
立即学习“PHP免费学习笔记(深入)”;
- 单字段简单清空:用
setField(),例如UserModel::where('status', 0)->setField('temp_data', null) - 多字段或需触发模型事件(如日志、验证、时间戳):必须用
update(),例如UserModel::where('updated_time', 'update(['temp_data' => null, 'cache_json' => '']) -
setField()不走模型生命周期:不触发beforeUpdate、不校验validate、不自动写入update_time,适合后台定时清理脚本 - 字段是 JSON 类型时,
null可能被驱动转成空数组,建议统一用空字符串''避免兼容问题
批量删大量数据要加锁,否则多实例会重复删
线上服务常部署多个 PHP-FPM 实例或容器,若清理脚本没加锁,同一时刻多个进程可能同时执行相同 delete 语句,轻则重复删、重则漏删(因 WHERE 条件被并发修改)。最简可靠的锁方案是文件锁:
- 删前获取独占锁:
file_put_contents(RUNTIME_PATH . 'clean_expired.lock', time(), LOCK_EX),失败则直接退出 - 删完立刻释放:
unlink(RUNTIME_PATH . 'clean_expired.lock'),别依赖脚本结束自动清理 - 别用
Db::table()->batchDelete()处理超万级数据——它底层仍是 IN 查询,容易超 MySQLmax_allowed_packet或触发慢查询;应分页循环删,每次 500–1000 条 - 上线前务必查测试库的
SELECT @@sql_mode,若含STRICT_TRANS_TABLES,日期字段为'0000-00-00 00:00:00'会导致 delete() 返回空结果且无提示
TP8.0 的 delete 行为比早期版本更严格,很多“以前能跑”的写法现在会静默失败或直接报错。关键不是记住哪个函数能用,而是明确每一步操作的目标:是删行、删字段值、还是打标记?目标错了,代码再短也没用。



















