设计表结构对于Java开发人员来说是必备的技能,表结构设计的好,可以提升我们的开发效率。设计表结构对于有一定开发经验的朋友来说并不困难,而优雅的设计表结构则是对工程师能力的一种考察,对于同一种E-R模型,根据不同的业务场景,设计出对应的好的表结构,则是对工程师的能力提出了更高要求。这篇文章,我们将以树形表结构设计为例子,引出针对不同的业务场景和变化多端的业务需求,我们怎么将树形结构的威力发挥到最大化。
考虑这样一个场景:我们现在面临一个这样的问题:我们现在拿到了全国所有省市区县的资料,现在要统计某个省或者某个市的生产总值,这个时候我们怎么来设计表结构呢?
刚开始不要对自己要求太高,我们先来看一种初级做法,假设我们要统计浙江省的生产总值,而我们拿到的数据,最底层的节点为区(县)即:只有在区(县)节点是有生产总值数据的,其它的节点需要汇总计算。我们可以这样设计表结构:(表中数据简化实际模型)
province_id | province_name |
1 | 北京 |
2 | 上海 |
3 | 广东 |
4 | 浙江 |
city_id | city_name | parent_id | total_gross |
11 | 东城区 | 1 | 20 |
21 | 黄浦区 | 2 | 15 |
32 | 深圳市 | 3 | |
41 | 杭州市 | 4 |
district_id | district_name | parent_id | total_gross |
321 | 福田区 | 32 | 8 |
411 | 上城区 | 41 | 7 |
412 | 下城区 | 41 | 6 |
322 | 罗湖区 | 32 | 7 |
观察表中的数据,我们发现要计算浙江的生产总值,我们就要计算杭州的生产总值,要计算杭州的生产总值我们就得计算上城区和下城区的总值,为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是方便我们进行向下查找的。
所以我们可以把表结构改造一下:
id | name | total_gross | parent_id |
0 | 中国 | ||
1 | 北京 | 0 | |
2 | 上海 | 0 | |
3 | 广东 | 0 | |
4 | 浙江 | 0 | |
11 | 东城区 | 20 | 1 |
21 | 黄埔区 | 15 | 2 |
32 | 深圳市 | 3 | |
41 | 杭州市 | 4 | |
321 | 福田区 | 8 | 32 |
322 | 罗湖区 | 7 | 32 |
411 | 上城区 | 7 | 41 |
412 | 下城区 | 6 | 41 |
经过改造,我们把原来的三张表存储数据,改成了一张表存储数据。这样一来,增加或删除层级,我们就不用新建或者删除表了。
我们知道Oracle 原生支持递归查询,而MySQL不支持,要自己写函数。我们把这个过程放在Java里做。把返回的数据构造成树结构
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.6.7
com.nightcat
tree_example
0.0.1-SNAPSHOT
tree_example
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.0
org.springframework.boot
spring-boot-starter-test
test
com.alibaba
druid
1.0.31
mysql
mysql-connector-java
5.1.47
org.projectlombok
lombok
1.16.22
org.springframework.boot
spring-boot-maven-plugin
src/main/java
**/*.xml
src/main/resources
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 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 getCityTree(Integer parentId);
List getFirstLevel();
}
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 getCityTree(Integer parentId) {
List cityList;
if (parentId == null) {
cityList = cityDao.getFirstLevel();
} else {
cityList = cityDao.getCityTree(parentId);
}
for (CityBean cityBean : cityList) {
Integer parentId1 = cityBean.getId();
List 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 getCityTree() {
return cityService.getCityTree(null);
}
}
返回的结果
这样我们把这样一个结构返回给前端,前端就能展示出一棵树了。
我们已经把数据加工成了一棵树返回给了前端,这样就万事大吉了么,显然不是,回到我们原来的问题:如何计算浙江省的生产总值,你会发现,如果按照我们上面的表设计来说,我们往往只能在递归的过程中去判断:
public List getCityTree(Integer parentId) {
List cityList;
if (parentId == null) {
cityList = cityDao.getFirstLevel();
} else {
cityList = cityDao.getCityTree(parentId);
}
for (CityBean cityBean : cityList) {
Integer parentId1 = cityBean.getId();
List cityTree = getCityTree(parentId1);
cityBean.setChildren(cityTree);
}
return cityList;
}
public Integer getTotalIncome(Integer parentId) {
Integer sum = 0;
List 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方法其实就是一个反向递归的过程
关于上面这个问题,我们是在程序中处理的,那么我们能不能通过另一种表设计的方式,让这个问题的处理变得简洁呢,答案是可以的
我们试着改变一下表结构,如下表
id | path | name | total_gross |
0 | 0/ | 中国 | |
1 | 0/1/ | 北京 | |
2 | 0/2/ | 上海 | |
3 | 0/3/ | 广东 | |
4 | 0/4/ | 浙江 | |
11 | 0/1/11 | 东城区 | 20 |
21 | 0/2/21 | 黄埔区 | 15 |
32 | 0/3/32 | 深圳 | |
41 | 0/4/41 | 杭州 | |
321 | 0/3/32/321 | 福田区 | 8 |
322 | 0/3/32/322 | 罗湖区 | 7 |
411 | 0/4/41/411 | 上城区 | 7 |
412 | 0/4/41/412 | 下城区 | 6 |
表设计成这样之后,你会发现,计算生产总值特别简单:
SELECT
sum(
ifnull( t.total_gross, 0 ))
FROM
tb_district_path t
WHERE
t.path LIKE '0/4%'
以上这种方法免去了我们在应用程序中处理计算总值的繁琐之处
当然,设计表结构需要我们在实践当中不断积累,这篇文章也只是树形表结构设计中的一小部分而已,考虑到阅读感受和篇幅,今天就写到这里。
我是扬灵,如果你想和我一起学习更多技术,欢迎 关注,点赞,评论,收藏,你们的鼓励是我创作的最大动力