Go + GORM 多级分类实现方案对比:内存建树、循环查询与 Preload

一、背景

多级分类(树形结构)在实际开发中的应用广泛。

常见问题:

  • 如何高效获取子分类

  • 避免 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"`
}

二、技术方案

  1. 循环查询(逐层查询)
    最容易理解的查询方式:先查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)
  1. 一次查询 + 内存建树(推荐方案)
    基本思路:一次性从表里将所有数据取出来,随后映射成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)
  1. 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": []
               }
          ]
     }
]
相关推荐
沐知全栈开发9 小时前
PHP MySQL 插入数据
开发语言
青云交10 小时前
Java 大视界 -- Java+Flink CDC 构建实时数据同步系统:从 MySQL 到 Hive 全增量同步(443)
java·mysql·flink·实时数据同步·java+flink cdc·mysql→hive·全增量同步
Victor35610 小时前
Hibernate(34)Hibernate的别名(Alias)是什么?
后端
小罗和阿泽10 小时前
Java项目 简易图书管理系统
java·开发语言
亮子AI10 小时前
【MySQL】node.js 如何判断连接池是否正确连接上了?
数据库·mysql·node.js
superman超哥10 小时前
Rust HashMap的哈希算法与冲突解决:高性能关联容器的内部机制
开发语言·后端·rust·哈希算法·编程语言·冲突解决·rust hashmap
刘一说10 小时前
腾讯位置服务JavaScript API GL与JavaScript API (V2)全面对比总结
开发语言·javascript·信息可视化·webgis
Victor35610 小时前
Hibernate(33) Hibernate的投影(Projections)是什么?
后端
a程序小傲10 小时前
【Node】单线程的Node.js为什么可以实现多线程?
java·数据库·后端·面试·node.js