gorm默认不支持递归树查询,因其链式api未覆盖with recursive等原生sql语法;preload仅支持单层关联,多层需手写原生sql+scan或内存构树。

为什么 GORM 默认不支持递归树查询
GORM 本身没有内置的 WITH RECURSIVE 支持,也不提供类似 Laravel 的 withCount('children') 或无限层级 load() 方法。直接用 Preload("Children") 只能查一层,嵌套多层就得手动写 N+1 查询或拼 SQL —— 这是绝大多数人卡住的第一步。
根本原因在于:关系型数据库的递归能力(如 PostgreSQL 的 WITH RECURSIVE、MySQL 8.0+ 的 CTE)需要手写原生 SQL 才能触发,而 GORM 的链式 API 天然不覆盖这类语法。
用 Raw SQL + Scan 实现单次递归查询(PostgreSQL / MySQL 8.0+)
最可控、性能最好的方式是绕过 GORM 的 ORM 层,用 db.Raw().Scan() 直接执行带 WITH RECURSIVE 的语句。关键不是“能不能”,而是“怎么让结果映射回 Go struct”。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 必须给递归结果加一个
level字段,否则无法还原层级顺序 - 返回的结构体字段名要和 SQL 中的别名严格一致(大小写敏感),例如
SELECT id, name, parent_id, level→ struct 字段得叫ID,Name,ParentID,Level - PostgreSQL 示例:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM categories c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree ORDER BY level, id;
- 执行时用:
db.Raw(sql).Scan(&results),results是[]CategoryNode类型,其中CategoryNode包含ID,Name,ParentID,Level
用 GORM 关联 + 内存构树(适合中小数据量)
如果数据库不支持 CTE(比如 MySQL 5.7),或树深度固定 ≤ 4 层、总节点数
- 先用
db.Find(&nodes)查出全部节点,按parent_id排序(避免父节点在子节点后出现) - 用
map[uint](*Node)缓存所有节点指针,遍历一次完成父子挂载 - 根节点识别靠
ParentID == 0或nil(根据你的字段定义) - 注意:不要用
Preload嵌套加载,它生成的是 LEFT JOIN,会爆炸式膨胀结果集;平铺查 + 内存构树反而更快更稳
自定义 GORM Scope 构建树形条件(如查某节点的完整路径)
查“某个分类的所有祖先”比查整棵树更常见,也更适合封装成可复用逻辑。这时候不该依赖递归 SQL,而该用循环查父级(最多 6~7 次),或者用闭包式 scope 配合 Where() 链式拼接。
- 定义 scope:
func WithAncestors(nodeID uint) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { var ids []uint current := nodeID for current != 0 { ids = append(ids, current) var parent uint db.Table("categories").Select("parent_id").Where("id = ?", current).Scan(&parent) current = parent } return db.Where("id IN ?", ids) } } - 使用:
var ancestors []Category; db.Scopes(WithAncestors(123)).Find(&ancestors) - 缺点:N 次查询;优点:完全兼容所有数据库,逻辑清晰,容易单元测试
- 进阶可改用单次
WHERE id = ? OR parent_id = ? OR ...拼接,但要注意参数数量限制(如 MySQL max_allowed_packet)
真正麻烦的从来不是“怎么写出第一版树查询”,而是“怎么让树查询在分页、搜索、软删除、多租户场景下依然正确”。这些边界问题没法靠一个 magic function 解决,得从数据建模阶段就决定:用 path 字段(如 /1/5/23)、lft/rgt,还是坚持靠递归 SQL —— 选哪种,决定了你后面半年要不要重写树逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










