Mysql閉包表之關於國家區域的一個實踐
阿新 • • 發佈:2018-11-12
在電商系統中,我們總是會遇到一些樹形結構資料的儲存需求。如地理區域、位置資訊儲存,地理資訊按照層級劃分,會分為很多層級,就拿中國的行政區域劃分為例,簡單的省-市-縣-鎮-村就要五個級別。如果系統涉及到跨境的國際貿易,那麼儲存的地理資訊層級會更加深。那麼如何正確合理地儲存這些資料,並且又能很好的適應各種查詢場景就成了我們需要考慮的問題,這次我們來考慮通過閉包表方案,來達到我們的儲存及查詢需求。
一、設計閉包表
閉包表由Closure Table翻譯而來,通過父節點、子節點、兩節點距離來描述一棵樹空間換時間的思想,Closure Table,一種更為徹底的全路徑結構,分別記錄路徑上相關結點的全展開形式。
區域基礎資訊表結構如下
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