树形数据的表结构设计

344 阅读2分钟

树形数据结构是一类重要的非线性数据结构。诸如vue-router的数据、windows的文件目录都是树形数据结构,在业务中也常涉及到类似的树形数据,下面以课程为例进行设计:

  • 课程:课程id、课程名
  • 课程有子课程,例如后端课程包含java、springboot、php、Cloud

为课程增加字段来标记其父课程:

  • 课程:课程id、课程名、父课程id

可得如下Course表:

idnamepid
1后端课0
2前端课0
3java1
4springboot1
5vue2
6javascript2

依据上表,可构造出如下数据:即扁平数据转树形数据

{
  course: [
    {
      id: 1,
      name: '后端课',
      pid: 0,
      children: [
        {
          id: 3,
          pid: 1,
          name: 'java'
        },
        {
          id: 4,
          pid: 1,
          name: 'springboot'
        }
      ]
    },
    {
      id: 2,
      name: '前端课',
      pid: 0,
      children: [
        {
          id: 5,
          pid: 2,
          name: 'vue'
        },
        {
          id: 6,
          pid: 2,
          name: 'javascript'
        }
      ]
    }
  ]
}

如果构造上述数据结构?

方案一: 两次查询, 对数据进行组装处理:

    let parentList = 查询到所有pid为0的项
    let childList = 查询到所有pid不为0的项
    parentList.map((item) => {
      item['children'] = [] // 构造子项
      childList.map((subItem) => {
        if (subItem.pid === item.id) {
          return item.children.push(subItem)
        }
      })
    })
    return parentList

方案二: 一次查询所有项, 进行扁平数组转Tree

let courseData = 所有项
return arrayToTree(courseData, 'id', 'pid', 'children')

/**
   * 将扁平数组结构转为Tree
   * @param {Array} items 要转换的数组
   * @param {String} idField 数据id
   * @param {String} pidField 数据父id
   * @param {String} childrenField 子项字段
   * @returns {Tree}
   */
  static arrayToTree(items, idField, pidField, childrenField) {
    const result = []
    const itemMap = {}

    for (const item of items) {
      const id = item[idField]
      const pid = item[pidField]
      if (!itemMap[id]) itemMap[id] = { [childrenField]: [] }
      itemMap[id] = { ...item, [childrenField]: itemMap[id][childrenField] }
      const treeItem = itemMap[id]
      if (pid === 0) result.push(treeItem)
      else {
        if (!itemMap[pid]) itemMap[pid] = { [childrenField]: [] }
        itemMap[pid][childrenField].push(treeItem)
      }
    }
    return result
  }
}

方案三: sql上进行优化, 即关联查询:

SELECT 
`Course`.`id`, 
`Course`.`name`, 
`Course`.`pid`, 
`subCourseList`.`id` AS `subCourseList.id`, 
`subCourseList`.`name` AS `subCourseList.name`, 
`subCourseList`.`pid` AS `subCourseList.pid`, 
FROM 
	`course` AS `Course` 
		LEFT OUTER JOIN 
	`course` AS `subCourseList` 
		ON 
	`Course`.`id` = `subCourseList`.`pid` 
WHERE `Course`.`pid` = 0 ;

注意: 数据量庞大时, 关联查询相比两次查询性能消耗更大, 因为关联查询要进行笛卡尔积, 一个不错的方案是第一次使用关联查询后将查询结果缓存到redis中, 之后从redis里读取查询结果。