1. 程式人生 > >Mysql閉包表之關於國家區域的一個實踐

Mysql閉包表之關於國家區域的一個實踐

在電商系統中,我們總是會遇到一些樹形結構資料的儲存需求。如地理區域、位置資訊儲存,地理資訊按照層級劃分,會分為很多層級,就拿中國的行政區域劃分為例,簡單的省-市-縣-鎮-村就要五個級別。如果系統涉及到跨境的國際貿易,那麼儲存的地理資訊層級會更加深。那麼如何正確合理地儲存這些資料,並且又能很好的適應各種查詢場景就成了我們需要考慮的問題,這次我們來考慮通過閉包表方案,來達到我們的儲存及查詢需求。

一、設計閉包表

閉包表由Closure Table翻譯而來,通過父節點、子節點、兩節點距離來描述一棵樹空間換時間的思想,Closure Table,一種更為徹底的全路徑結構,分別記錄路徑上相關結點的全展開形式。

能明晰任意兩結點關係而無須多餘查詢,級聯刪除和結點移動也很方便。但是它的儲存開銷會大一些,除了表示結點的Meta資訊,還需要一張專用的關係表。

區域基礎資訊表結構如下

CREATE TABLE `area_base` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `area_name` varchar(50) NOT NULL COMMENT '區域名稱',
  `sequence` int(11) DEFAULT NULL COMMENT '排序號,越小越靠前',
  `created_by` bigint(20) NOT NULL COMMENT '建立人',
  `created_time` bigint(20) NOT NULL COMMENT '建立時間',
  `updated_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `updated_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新時間',
  `is_del` tinyint(2) NOT NULL DEFAULT '0' COMMENT '狀態:0 正常,-1 已刪除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COMMENT='區域表';

區域之間指向關係的閉包表結構如下

CREATE TABLE `area_closure` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增長Id',
`ancestor` bigint(20) NOT NULL COMMENT '祖先',
`descendant` bigint(20) NOT NULL COMMENT '後代',
`distance` int(11) DEFAULT NULL COMMENT '祖先到後代之間的距離',
PRIMARY KEY (`id`),
UNIQUE KEY `id_ancedesc` (`ancestor`,`descendant`) USING BTREE,
KEY `idx_ancestor` (`ancestor`,`distance`) USING BTREE,
KEY `idx_descendant` (`descendant`,`distance`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=259 DEFAULT CHARSET=utf8mb4 COMMENT='區域的樹形結構閉包表';

模擬一些示範資料,如下所示

mysql> select * from area_base;
+----+-----------+----------+------------+----------------+------------+---------------+--------+
| id | area_name | sequence | created_by | created_time   | updated_by | updated_time  | is_del |
+----+-----------+----------+------------+----------------+------------+---------------+--------+
|  1 | 根節點    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 29 | 亞洲      |       96 |        123 | 15679841561561 |        990 | 1540031478909 |      0 |
| 30 | 美洲      |       33 |        123 | 15679841561561 |        990 | 1540031478923 |      0 |
| 31 | 歐洲      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 35 | 中國      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 36 | 日本      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 37 | 朝鮮      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 38 | 廣東省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 39 | 新疆省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 40 | 廣西省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 41 | 深圳市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 42 | 廣州市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 43 | 佛山市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
+----+-----------+----------+------------+----------------+------------+---------------+--------+
13 rows in set

二、閉包表中的遞迴操作

如何遞迴構造出一顆全區域的返回樹

    public AreaTreeResponse getAreaTree(Long areaId) {
        String cacheKey = BasicConst.Cache.AREA_TREE_KEY + BasicConst.AreaInfo.ROOT_NODE_ID;
        AreaTreeResponse areaTreeResponse = cache.get(cacheKey);
        if(areaTreeResponse != null){
            return areaTreeResponse;
        }
        // 遞迴生成
        areaTreeResponse = newAreaTreeByRecur(areaId);
        // 加入快取,並設定超時時間
        cache.set(cacheKey, areaTreeResponse, BasicConst.Cache.AREA_CACHE_TTL);
        return areaTreeResponse;
    }
    /**
     * 根據父節點構造返回子樹
     *
     * @param parentId
     * @return
     */
    private AreaTreeResponse newAreaTreeByRecur(Long parentId){
        // 初始化返回結果
        AreaTreeResponse areaTree = new AreaTreeResponse();
        // 獲取直接子節點
        List<AreaTree> areaChildList = areaClosureMapper.getAreaTree(parentId, 1);
        if(areaChildList == null || areaChildList.size() == 0){
            return areaTree;
        } else {
            // 初始化當前節點的id和name
            Long curNodeId = null;
            String curNodeName = null;
            // 初始化當前節點對應的childList
            List<AreaTreeResponse> childList = new ArrayList<>();
            for (AreaTree areaChildNode : areaChildList) {
                curNodeId = areaChildNode.getParentId();
                curNodeName = areaChildNode.getParentName();
                // 遞迴,將子節點當成父節點向下遞迴
                AreaTreeResponse child = newAreaTreeByRecur(areaChildNode.getChildrenId());
                // 葉子節點設定child
                child.setAreaId(areaChildNode.getChildrenId());
                child.setAreaName(areaChildNode.getChildrenName());
                childList.add(child);
            }
            // 將childList傳給上一節點
            areaTree.setAreaId(curNodeId);
            areaTree.setAreaName(curNodeName);
            areaTree.setChildren(childList);
            return areaTree;
        }
    }

寫一個測試用例進行測試

@Test
public void getCurrentNodeTree(){
    AreaTreeResponse areaTreeResponse = areaService.getAreaTree(1L);
    // 模擬返回樹
    String jsonObject = JSONObject.toJSONString(areaTreeResponse);
    System.out.println("lingyejun test result :"+jsonObject);
}

遞迴生成的樹狀Json如下

{
    "areaId":1,
    "areaName":"根節點",
    "children":[
        {
            "areaId":31,
            "areaName":"歐洲"
        },
        {
            "areaId":30,
            "areaName":"美洲"
        },
        {
            "areaId":29,
            "areaName":"亞洲",
            "children":[
                {
                    "areaId":35,
                    "areaName":"中國",
                    "children":[
                        {
                            "areaId":38,
                            "areaName":"廣東省",
                            "children":[
                                {
                                    "areaId":41,
                                    "areaName":"深圳市"
                                },
                                {
                                    "areaId":42,
                                    "areaName":"廣州市"
                                },
                                {
                                    "areaId":43,
                                    "areaName":"佛山市"
                                }
                            ]
                        },
                        {
                            "areaId":39,
                            "areaName":"新疆省"
                        },
                        {
                            "areaId":40,
                            "areaName":"廣西省"
                        }
                    ]
                },
                {
                    "areaId":36,
                    "areaName":"日本"
                },
                {
                    "areaId":37,
                    "areaName":"朝鮮"
                }
            ]
        }
    ]
}

參考文章:https://www.biaodianfu.com/closure-table.html