一、背景
多级分类(树形结构)在实际开发中的应用广泛。
常见问题:
-
如何高效获取子分类
-
避免 N+1 查询
-
支持多层级/无限层级
本文介绍三种实现方式,并对比优缺点。
二、数据结构
plain data

model
go
package model
type Category struct {
Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Name string `gorm:"column:name;not null;type:varchar(50)" json:"name"`
Level uint8 `gorm:"column:level;not null" json:"level"`
ParentId int64 `gorm:"column:parent_id;not null" json:"parent_id"`
}
func (Category) TableName() string {
return "category"
}
handler
go
package main
func ToStringLog(v interface{}) {
b, _ := json.Marshal(v)
fmt.Println(string(b))
}
type CategoryListResponse struct {
Category []*CategoryData `json:"category"`
}
type CategoryData struct {
Id int64 `json:"id"`
Name string `json:"name"`
Level uint8 `json:"level"`
ParentId int64 `json:"parent_id"`
SubCategory []*CategoryData `json:"sub_category"`
}
二、技术方案
- 循环查询(逐层查询)
最容易理解的查询方式:先查level为1的分类,拿到一级分类的id,然后遍历这些id,找哪些分类的parent_id是这些id,并将这些分类作为一级分类的子分类。
优点:逻辑清晰
缺点:性能问题堪忧,存在遍历执行sql查询问题,而且当层级增加时,代码层级很深难以维护
go
var categories []model.Category
var categoryDataList []*CategoryData
// 1. 查询一级分类
db.Where("level = ?", 1).Find(&categories)
for _, cat := range categories {
// 2. 查询该一级分类下的二级分类
var subCategories []model.Category
db.Where("level = ? AND parent_id = ?", 2, cat.Id).
Find(&subCategories)
// 3. 组装二级分类数据
var subCategoryDataList []*CategoryData
for _, sub := range subCategories {
subCategoryDataList = append(subCategoryDataList, &CategoryData{
Id: sub.Id,
Name: sub.Name,
Level: sub.Level,
ParentId: sub.ParentId,
})
}
// 4. 组装一级分类
categoryDataList = append(categoryDataList, &CategoryData{
Id: cat.Id,
Name: cat.Name,
Level: cat.Level,
ParentId: cat.ParentId,
SubCategory: subCategoryDataList,
})
}
// 5. 最终 response
resp := CategoryListResponse{
Category: categoryDataList,
}
ToStringLog(resp)
- 一次查询 + 内存建树(推荐方案)
基本思路:一次性从表里将所有数据取出来,随后映射成map,key是每一条数据的id,value是这条数据。随后对map进行遍历,检查他的parent_id是否存在,如果不存在,证明他是一级分类,直接将其放到分类数组中即可。如果parent_id存在,证明他是子分类,那么通过map[parent_id]可以拿到父分类,将自己加入到父分类的subCategory数组中即可。
go
var categoies []model.Category
var categoryDataList []*CategoryData
result := db.Find(&categoies)
if result.RowsAffected < 1 {
panic("error")
}
db.Model(&model.Category{}).Find(&categoies)
nodeMap := make(map[int64]*CategoryData)
for _, c := range categoies {
nodeMap[c.Id] = &CategoryData{
Id: c.Id,
Name: c.Name,
Level: c.Level,
ParentId: c.ParentId,
}
}
for _, n := range nodeMap {
if n.ParentId == 0 {
categoryDataList = append(categoryDataList, n)
} else {
parent := nodeMap[n.ParentId]
if parent != nil {
parent.SubCategory = append(parent.SubCategory, n)
}
}
}
ToStringLog(categoryDataList)
- GORM Preload
优点:
- 自动挂载子节点
- 代码简洁。
缺点:
- 只能处理固定层级
首先需要更改model,设置自引用关系.
model
go
type Category struct {
Id int64
Name string
Level uint8
ParentId int64
SubCategory []*Category `gorm:"foreignKey:ParentId" json:"sub_category"`
}
handler
go
var categoies []model.Category
db.Model(&model.Category{}).Where("level = 1").Preload("SubCategory").Preload("SubCategory.SubCategory").Find(&categoies)
ToStringLog(categoies)
最终输出JSON:
json
[
{
"id": 1,
"name": "技术",
"level": 1,
"parent_id": 0,
"sub_category": [
{
"id": 6,
"name": "前端开发",
"level": 2,
"parent_id": 1,
"sub_category": [
{
"id": 21,
"name": "IOS开发",
"level": 3,
"parent_id": 6,
"sub_category": null
},
{
"id": 22,
"name": "Andriod开发",
"level": 3,
"parent_id": 6,
"sub_category": null
}
]
},
{
"id": 7,
"name": "后端开发",
"level": 2,
"parent_id": 1,
"sub_category": []
},
{
"id": 8,
"name": "移动开发",
"level": 2,
"parent_id": 1,
"sub_category": []
},
{
"id": 9,
"name": "人工智能",
"level": 2,
"parent_id": 1,
"sub_category": []
},
{
"id": 10,
"name": "数据库",
"level": 2,
"parent_id": 1,
"sub_category": []
}
]
},
{
"id": 2,
"name": "产品",
"level": 1,
"parent_id": 0,
"sub_category": [
{
"id": 11,
"name": "产品经理",
"level": 2,
"parent_id": 2,
"sub_category": []
},
{
"id": 12,
"name": "需求分析",
"level": 2,
"parent_id": 2,
"sub_category": []
},
{
"id": 13,
"name": "用户研究",
"level": 2,
"parent_id": 2,
"sub_category": []
}
]
},
{
"id": 3,
"name": "设计",
"level": 1,
"parent_id": 0,
"sub_category": [
{
"id": 14,
"name": "UI设计",
"level": 2,
"parent_id": 3,
"sub_category": []
},
{
"id": 15,
"name": "交互设计",
"level": 2,
"parent_id": 3,
"sub_category": []
},
{
"id": 16,
"name": "视觉设计",
"level": 2,
"parent_id": 3,
"sub_category": []
}
]
},
{
"id": 4,
"name": "运营",
"level": 1,
"parent_id": 0,
"sub_category": [
{
"id": 17,
"name": "内容运营",
"level": 2,
"parent_id": 4,
"sub_category": []
},
{
"id": 18,
"name": "用户运营",
"level": 2,
"parent_id": 4,
"sub_category": []
}
]
},
{
"id": 5,
"name": "职场",
"level": 1,
"parent_id": 0,
"sub_category": [
{
"id": 19,
"name": "职业规划",
"level": 2,
"parent_id": 5,
"sub_category": []
},
{
"id": 20,
"name": "面试技巧",
"level": 2,
"parent_id": 5,
"sub_category": []
}
]
}
]