thinkphp 无法用 with('children.children') 实现无限级分类树,因预加载仅支持固定层级、易触发 n+1 或 sql 超限;推荐全量查询后 php 递归构建树,并严格处理根节点判断、缓存更新与前端字段统一。

为什么不能用 with + hasMany 实现无限级
ThinkPHP 的 with 预加载只支持固定层级(如两级),不支持动态深度嵌套。即使定义了 children 关系:return $this->hasMany('Category','pid','id')->order('sort');,调用 with('children.children.children') 也只会查到三层,且每层都发新 SQL——数据量稍大就卡死。
- TP6/TP8 均未实现递归式预加载,官方文档也无此能力说明
-
with底层是按关系字段做 JOIN 或 IN 查询,无法自动推导“子的子的子…”的 ID 集合 - 若强行写多层
with,生成的 SQL 会极冗长,MySQL 可能报max_join_size错误
推荐做法:全量查出 + PHP 递归 buildTree
先用一次 Db::table('category')->select() 拿到全部分类,再在 PHP 层按 pid 和 id 构建树。这是最稳定、可控、易调试的方式。
- 务必确认真实根节点的
pid值:可能是0、null或空字符串,别硬写where('pid', 0) - 构建函数里统一用
'children' => []作键名,避免前端遍历时出现undefined - 示例片段:
function buildTree($items, $parentId = null) {<br> $tree = [];<br> foreach ($items as $item) {<br> if ($item['pid'] === $parentId) {<br> $item['children'] = buildTree($items, $item['id']);<br> $tree[] = $item;<br> }<br> }<br> return $tree;<br>} - 注意递归深度:数据超 200 行、层级 > 8 时,建议改用迭代法(用栈模拟递归),防 PHP 栈溢出
缓存必须加,但缓存键设计很关键
分类树变更频率低,但每次访问都执行全量查 + 递归组装,对 CPU 和内存压力明显。加缓存能立竿见影,但键设计错了等于没加。
- 缓存键建议包含业务标识 + 数据版本号,例如:
'category_tree_v2',而非简单用'category_tree' - 类目新增/编辑/删除后,必须主动删掉该缓存,不能等过期——否则前端看到的是旧树
- 不要用
Cache::get('category_tree') ?: buildTree(...)这种懒加载,高并发下可能击穿 DB;应改为「先删缓存,再重建并 set」 - 如果用了 Redis,可用
SETNX+ 过期时间防雪崩,但 TP 自带缓存驱动通常已处理基础逻辑
别碰 MySQL 8.0+ CTE,除非你真需要路径查询
网上有些教程教用 WITH RECURSIVE 写 CTE 查询树,看起来很酷,但实际落地问题多。
- 执行
SELECT VERSION()确认数据库版本,低于 8.0 直接报错 - CTE 无法被 ThinkPHP 的 Query Builder 原生支持,得手写原生 SQL,失去模型校验、软删除、作用域等能力
- 只适合「查某节点的所有祖先」或「查某节点所有后代」这类单点路径需求,不适合首页渲染整棵树
- 如果后续要加排序(如按
sort字段)、状态过滤(如status=1),CTE 会迅速变得难维护
pid 为 null 的根类目、pid 指向不存在 ID 的脏数据)跑一遍,比看十篇教程都管用。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











