八千字超硬核树形表结构设计,中级开发必看

问题背景

设计表结构对于Java开发人员来说是必备的技能,表结构设计的好,可以提升我们的开发效率。设计表结构对于有一定开发经验的朋友来说并不困难,而优雅的设计表结构则是对工程师能力的一种考察,对于同一种E-R模型,根据不同的业务场景,设计出对应的好的表结构,则是对工程师的能力提出了更高要求。这篇文章,我们将以树形表结构设计为例子,引出针对不同的业务场景和变化多端的业务需求,我们怎么将树形结构的威力发挥到最大化。

基础准备

我们为什么需要树形结构?

考虑这样一个场景:我们现在面临一个这样的问题:我们现在拿到了全国所有省市区县的资料,现在要统计某个省或者某个市的生产总值,这个时候我们怎么来设计表结构呢?

刚开始不要对自己要求太高,我们先来看一种初级做法,假设我们要统计浙江省的生产总值,而我们拿到的数据,最底层的节点为区(县)即:只有在区(县)节点是有生产总值数据的,其它的节点需要汇总计算。我们可以这样设计表结构:(表中数据简化实际模型)

tb_province
province_idprovince_name
1北京
2上海
3广东
4浙江

                      

tb_city
city_idcity_nameparent_idtotal_gross
11东城区120
21黄浦区215
32深圳市3
41杭州市4

                    

tb_district
district_iddistrict_nameparent_idtotal_gross
321福田区328
411上城区417
412下城区416
322罗湖区327

观察表中的数据,我们发现要计算浙江的生产总值,我们就要计算杭州的生产总值,要计算杭州的生产总值我们就得计算上城区和下城区的总值,为7+6=13.

那么我们该如何编写这条SQL呢?

首先我们要将三张表联合成一张表,然后取tb_province中provide_id为4的数据,最后对生产总值字段进行汇总.

SELECT
	t.province_id,
	t.province_name,
	SUM( t2.total_gross ) 
FROM
	tb_province t
	JOIN tb_city t1 ON t.province_id = t1.parent_id
	JOIN tb_district t2 ON t1.city_id = t2.parent_id 
WHERE
	t.province_name = '浙江' 
GROUP BY
	province_id;

存在的问题

  • 如果要添加新的层级,那我们就得新建表,然后与已知的层级表进行关联
  • 如果要删除一个层级,我们就得删除一张表

用树形结构改造

我们前面提到了两个问题,如何解决这两个问题呢,即让我们在增加和删除层级时,不需要新建或者删除表,这就要用到我们说的树形结构了.。

我们发现,上面的三张表都有id 作为当前记录的唯一表示 parent_id 字段,用来表示当前节点的父节点,一个业务字段total_gross,其它的信息相对来说不重要,因为我们在业务开发时主要关注total_gross字段,而parent_id是方便我们进行向下查找的。

所以我们可以把表结构改造一下:

tb_district_tree
idnametotal_grossparent_id
0中国
1北京0
2上海0
3广东0
4浙江0
11东城区201
21黄埔区152
32深圳市3
41杭州市4
321福田区832
322罗湖区732
411上城区741
412下城区641

经过改造,我们把原来的三张表存储数据,改成了一张表存储数据。这样一来,增加或删除层级,我们就不用新建或者删除表了。

查找数据

我们知道Oracle 原生支持递归查询,而MySQL不支持,要自己写函数。我们把这个过程放在Java里做。把返回的数据构造成树结构

准备一个SpringBoot工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nightcat</groupId>
    <artifactId>tree_example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>tree_example</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.31</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>

</project>
server.port=8082
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.configuration.map-underscore-to-camel-case=true
package com.nightcat.tree_example.bean;

import lombok.Data;

import java.util.List;

@Data
public class CityBean {
    private Integer id;
    private String name;
    private Integer parentId;
    private Integer totalGross;
    private List<CityBean> children;
}
package com.nightcat.tree_example.dao;

import com.nightcat.tree_example.bean.CityBean;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CityDao {
    List<CityBean> getCityTree(Integer parentId);

    List<CityBean> getFirstLevel();

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nightcat.tree_example.dao.CityDao">
    <select id="getFirstLevel" resultType="com.nightcat.tree_example.bean.CityBean">
         select * from tb_district_tree where parent_id is null
    </select>
    <select id="getCityTree" resultType="com.nightcat.tree_example.bean.CityBean">
        select * from tb_district_tree where parent_id =#{parentId}
    </select>
</mapper>
package com.nightcat.tree_example.service;

import com.nightcat.tree_example.bean.CityBean;
import com.nightcat.tree_example.dao.CityDao;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class CityService {
    @Resource
    private CityDao cityDao;


    public List<CityBean> getCityTree(Integer parentId) {
        List<CityBean> cityList;
        if (parentId == null) {
            cityList = cityDao.getFirstLevel();
        } else {
            cityList = cityDao.getCityTree(parentId);
        }
        for (CityBean cityBean : cityList) {
            Integer parentId1 = cityBean.getId();
            List<CityBean> cityTree = getCityTree(parentId1);
            cityBean.setChildren(cityTree);
        }

        return cityList;
    }
}
package com.nightcat.tree_example.controller;

import com.nightcat.tree_example.bean.CityBean;
import com.nightcat.tree_example.service.CityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/hello/tree")
public class CityController {
    @Autowired
    private CityService cityService;

    @GetMapping("/list")
    public List<CityBean> getCityTree() {
        return cityService.getCityTree(null);
    }
}

返回的结果

 

 这样我们把这样一个结构返回给前端,前端就能展示出一棵树了。

如何计算总值?

我们已经把数据加工成了一棵树返回给了前端,这样就万事大吉了么,显然不是,回到我们原来的问题:如何计算浙江省的生产总值,你会发现,如果按照我们上面的表设计来说,我们往往只能在递归的过程中去判断:

  • 传入浙江省的id
  • 得到浙江省的所有子节点,如果该节点的total_gross是null,把它当成零处理
  • 如果子节点的total_gross不是null,那么我们把它加起来计算总值
public List<CityBean> getCityTree(Integer parentId) {
        List<CityBean> cityList;
        if (parentId == null) {
            cityList = cityDao.getFirstLevel();
        } else {
            cityList = cityDao.getCityTree(parentId);
        }
        for (CityBean cityBean : cityList) {
            Integer parentId1 = cityBean.getId();
            List<CityBean> cityTree = getCityTree(parentId1);
            cityBean.setChildren(cityTree);
        }

        return cityList;
    }

    public Integer getTotalIncome(Integer parentId) {
        Integer sum = 0;
        List<CityBean> cityTree = getCityTree(parentId);
        for (CityBean cityBean : cityTree) {
            Integer totalGross = cityBean.getTotalGross();
            if (totalGross == null) {
                Integer id = cityBean.getId();
                Integer totalIncome = getTotalIncome(id);
                sum += totalIncome;
            } else {
                sum += totalGross;
            }
        }
        return sum;
    }
}

你会发现getTotalIncome方法其实就是一个反向递归的过程

还有其它办法么?

关于上面这个问题,我们是在程序中处理的,那么我们能不能通过另一种表设计的方式,让这个问题的处理变得简洁呢,答案是可以的

另一种树形表结构

我们试着改变一下表结构,如下表

tb_district_path
idpathnametotal_gross
00/中国
10/1/北京
20/2/上海
30/3/广东
40/4/浙江
110/1/11东城区20
210/2/21黄埔区15
320/3/32深圳
410/4/41杭州
3210/3/32/321福田区8
3220/3/32/322罗湖区7
4110/4/41/411上城区7
4120/4/41/412下城区6

表设计成这样之后,你会发现,计算生产总值特别简单:

SELECT
	sum(
	ifnull( t.total_gross, 0 )) 
FROM
	tb_district_path t 
WHERE
	t.path LIKE '0/4%'

以上这种方法免去了我们在应用程序中处理计算总值的繁琐之处

写在结尾

当然,设计表结构需要我们在实践当中不断积累,这篇文章也只是树形表结构设计中的一小部分而已,考虑到阅读感受和篇幅,今天就写到这里。

我是扬灵,如果你想和我一起学习更多技术,欢迎 关注,点赞,评论,收藏,你们的鼓励是我创作的最大动力

  • 64
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
treegrid插件 当前选中的行: var config = { id: "tg1", width: "800", renderTo: "div1", headerAlign: "left", headerHeight: "30", dataAlign: "left", indentation: "20", folderOpenIcon: "images/folderOpen.gif", folderCloseIcon: "images/folderClose.gif", defaultLeafIcon: "images/defaultLeaf.gif", hoverRowBackground: "false", folderColumnIndex: "1", itemClick: "itemClickEvent", columns:[ {headerText: "", headerAlign: "center", dataAlign: "center", width: "20", handler: "customCheckBox"}, {headerText: "名称", dataField: "name", headerAlign: "center", handler: "customOrgName"}, {headerText: "拼音码", dataField: "code", headerAlign: "center", dataAlign: "center", width: "100"}, {headerText: "负责人", dataField: "assignee", headerAlign: "center", dataAlign: "center", width: "100"}, {headerText: "查看", headerAlign: "center", dataAlign: "center", width: "50", handler: "customLook"} ], data:[ {name: "城区分公司", code: "CQ", assignee: "", children:[ {name: "城区卡品分销中心"}, {name: "先锋服务厅", children:[ {name: "chlid1"}, {name: "chlid2"}, {name: "chlid3", children: [ {name: "chlid3-1"}, {name: "chlid3-2"}, {name: "chlid3-3"}, {name: "chlid3-4"} ]} ]}, {name: "半环服务厅"} ]}, {name: "清新分公司", code: "QX", assignee: "", children:[]}, {name: "英德分公司", code: "YD", assignee: "", children:[]}, {name: "佛冈分公司", code: "FG", assignee: "", children:[]} ] }; /* 单击数据行后触发该事件 id:行的id index:行的索引。 data:json格式的行数据对象。 */ function itemClickEvent(id, index, data){ window.location.href="ads"; } /* 通过指定的方法来自定义栏数据 */ function customCheckBox(row, col){ return ""; } function customOrgName(row, col){ var name = row[col.dataField] || ""; return name; } function customLook(row, col){ return "查看"; } //创建一个组件对象 var treeGrid = new TreeGrid(config); treeGrid.show(); /* 展开、关闭所有节点。 isOpen=Y示展开,isOpen=N示关闭 */ function expandAll(isOpen){ treeGrid.expandAll(isOpen); } /* 取得当前选中的行,方法返回TreeGridItem对象 */ function selectedItem(){ var treeGridItem = treeGrid.getSelectedItem(); if(treeGridItem!=null){ //获取数据行属性值 //alert(treeGridItem.id + ", " + treeGridItem.index + ", " + treeGridItem.data.name); //获取父数据行 var parent = treeGridItem.getParent(); if(parent!=null){ //jQuery("#currentRow").val(parent.data.name); } //获取子数据行集 var children = treeGridItem.getChildren(); if(children!=null && children.length>0){ jQuery("#currentRow").val(children[0].data.name); } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜猫nightcat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值