yii框架多级分类必须一次性查出全部数据再递归构造树,不可在递归中反复查询数据库,否则100条分类可能触发上百次sql,导致504超时;应先findall()->asarray()->all(),再用_buildtree本地递归,层级lev参数传递实时计算。

直接说结论:Yii 框架做多级分类,**必须一次性查出全部数据再递归构造树,不能在递归里反复查数据库**。否则 100 条分类可能触发上百次 SQL 查询,接口直接卡死或超时。
为什么不能在递归函数里用 find()->where(['parent_id' => $id])?
这是新手最常踩的坑——每进一层递归就执行一次数据库查询。比如顶级分类有 5 个,每个下有 3 个子类,再下又有 2 层……实际 SQL 调用次数是指数级增长的。
- 现象:
504 Gateway Timeout或日志里出现大量重复SELECT * FROM category WHERE parent_id = ? - 原因:ActiveRecord 每次
find()都走完整查询链路,含连接、解析、权限校验等开销 - 兼容性影响:在 MySQL 8.0+ 或 PostgreSQL 上更明显,连接池容易被打满
推荐做法:先 findAll(),再用 _buildTree() 本地递归
核心思路是把数据库压力卸载到 PHP 内存,用数组索引代替 SQL JOIN。适用于 5000 条以内的分类数据(超出需考虑闭包表或 path 字段)。
- 模型中写一个静态方法,返回扁平数组:
self::find()->asArray()->all() - 控制器里调用递归构造函数,传入整个数组和初始
$pid = 0 - 递归函数内部只做
foreach+==判断,不碰数据库 - 示例片段:
private static function _buildTree($items, $pid = 0) { $tree = []; foreach ($items as $item) { if ($item['parent_id'] == $pid) { $item['children'] = self::_buildTree($items, $item['id']); $tree[] = $item; } } return $tree; }
生成带缩进的下拉选项时,lev 字段怎么算?
不要依赖数据库里的 level 字段(容易脏),在递归过程中实时计算层级更可靠。
- 把
$lev作为参数传入递归函数,每次调用加 1 - 视图中用
str_repeat('—', $v['lev'])拼前缀,比 CSS 缩进更可控 - 注意:
lev从 1 开始(顶级为 1),不是从 0;否则array_unshift()插入“请选择”时层级会错位 - 常见错误:
str_repeat('--', $v['lev'] - 1)忘记减 1,导致顶级显示成--安徽
真正麻烦的不是递归逻辑本身,而是后续的「新增/移动/删除」操作如何保持树结构一致性——比如把子分类拖到另一个父类下,要批量更新 parent_id 和所有后代的 path(如果用了 path 方案)或重新计算 lev。这部分没做事务包裹,很容易出现半截树。











