ThinkPHP 8.0中软删除主模型关联查询为空是正常行为,需显式调用withTrashed()作用于关联模型:一是在关联定义中硬编码->withTrashed();二是用闭包预载动态指定;三是慎用withoutGlobalScope跳过全局作用域。

ThinkPHP 8.0 中启用软删除的模型,执行 with('posts') 关联查询时,即使主记录已被软删除,关联数据仍返回空数组或 null,这不是 Bug,而是框架默认行为——withTrashed() 不自动透传到关联模型,必须显式干预才能让已软删除的关联数据也可见。
主模型查得到,关联却为空?先确认 withTrashed() 是否生效
第一步:在主模型查询链最开头调用 withTrashed(),例如 User::withTrashed()->with('posts')->find(1)。这一步只解决主模型本身能查到已删记录的问题,但 posts 依然受其自身模型软删除规则限制。
第二步:用 fetchSql(true) 检查生成的 SQL,确认是否包含 delete_time IS NULL 条件。若 SQL 中仍有该条件,说明关联模型未被覆盖过滤——此时主模型的 withTrashed() 对它完全无效。
第三步:单独测试关联模型能否查出已删数据,运行 Post::onlyTrashed()->where('user_id', 1)->select()。如果返回空,问题不在关联调用逻辑,而在 Post 模型本身未启用 SoftDelete 或字段配置错误。
立即学习“PHP免费学习笔记(深入)”;
方法一:在关联定义中硬编码 withTrashed()
打开 User 模型,在 posts() 关联方法里直接追加 withTrashed():
public function posts(){ return $this->hasMany(Post::class)->withTrashed();}
这样每次通过 $user->posts 或 User::with('posts') 查询时,Post 模型都会自动移除 delete_time IS NULL 条件。注意:Post 模型必须已引入 SoftDelete trait 并正确配置 $deleteTime 字段,否则 withTrashed() 调用静默失效。
方法二:闭包预载动态指定 withTrashed()
不修改模型代码,改用闭包方式在查询时临时控制关联行为:
User::withTrashed()->with(['posts' => function ($query) { $query->withTrashed();find(1);
这种写法适用于临时调试、后台回收站页面等场景,避免污染模型通用逻辑。它比方法一更灵活,但每次调用都要重复写闭包,不适合高频复用。
注意:闭包内必须调用 $query->withTrashed(),不能写成 Post::withTrashed()——后者会脱离当前查询上下文,导致关联加载失败。
方法三:强制跳过全局作用域(慎用)
若关联模型启用了自定义全局作用域(如重写了 baseScope()),且该作用域内又手动加了 where('delete_time', null),则 withTrashed() 可能被二次覆盖。
此时可绕过整个 SoftDelete 全局作用域:
User::withTrashed()->with(['posts' => function ($query) { $query->withoutGlobalScope(SoftDelete::class);find(1);
这相当于彻底关闭 Post 模型的软删除过滤机制,所有记录(含已删)都会被拉取。但风险极高:若 Post 表存在敏感已删数据,此操作会直接暴露,仅限开发环境验证或特定审计导出场景使用。
关联恢复时 withTrashed() 仍不生效?检查字段类型与 NULL 写入能力
第一步:确认 Post 表的 delete_time 字段是 DATETIME NULL 或 BIGINT NULL,严禁设为 NOT NULL DEFAULT '0000-00-00' —— MySQL 严格模式下插入 NULL 会失败,导致软删除状态无法写入,后续 withTrashed() 查不到任何“已删”数据。
第二步:检查 Post 模型中是否声明了 protected $type = ['delete_time' => 'integer']。若字段是整型时间戳却没加该声明,框架会把 null 强转成字符串再转成 0,最终仍被判定为“已删除”,withTrashed() 返回结果看似正常,实则逻辑错乱。
第三步:执行 Post::withTrashed()->count() 与 Post::count() 对比数量。若两者相等,说明根本没有数据真正进入软删除状态——问题根源在删除动作本身,而非查询阶段。



















