在關係型資料庫中優雅的處理組織架構樹
我們在專案開發中經常會遇到這樣的場景:企業組織架構,回覆評論鏈,商品類目等樹型結構。本文結合網上一些資料,以及自己在專案中的實踐來說一說幾種處理樹型結構的方式。
可能有不少小夥伴遇到該問題,第一個想法就是我在每條資料上都儲存它的parent_id
就可以滿足需求了啊,這樣一層一層都能找到, 寫一個遞迴演演算法就可以拿到他的所有下級或者上級。這是一種可以實現的方式,我們先不評價這種方式的優劣。
下面先說下幾種常見的處理方式:
- Adjacency list (鄰接表)
- Closure table (閉包表)
- Path enumeration (路徑列舉)
- Nested Sets(巢狀集)
本文主要講解Path enumeration (路徑列舉),其它幾種只做簡單的展示,有需要了解的可以點選一下連線檢視
-
對Closure table 講解詳細www.jianshu.com/p/951b742fd…
-
四種方式都有講解 www.cnblogs.com/wjq310/p/88…
-
MySQL實現巢狀集合模型 yq.aliyun.com/articles/50…
Adjacency list (鄰接表)
下圖是一個簡單的鄰接表結構的組織結構,也就是前面我們的說方式
獲取資料的方式
- 獲取某組織下的直屬部門,我們只需根據組織的id查詢即可
select * from org where parent_id =1;
複製程式碼
- 查詢某組織下的所有組織,需要遞迴的+迴圈拿到子、孫...的下級節點,大概寫法如下:
public List<Org> getOrgTree(String parentId){
List<Orgs> resultOrgs = new ArrayList<>();
// 拿到下級組織
List<Org> children = getChildren(parentId);
resultOrgs.addAll(children);
for(Org org :children){
resultOrgs.addAll(getOrgTree(org.parentID));
}
return resultOrgs;
}
複製程式碼
Closure table (閉包表)
這種方式用帖子評論來介紹,資料構造麻煩,借用了www.jianshu.com/p/951b742fd…中的圖,資料庫模型如下:
![](xiao-files.oss-cn-beijing.aliyuncs.com/picgo/閉包表 (2).png)
ancestor 儲存回覆的根Id,descendant 儲存當前Id,中儲存佔空間,理論上講需要O(n²)的空間來儲存關係。
-- 父查子,連表查詢comment 和 comment_path 拿到4號的評論
select c.* from comment c
left join comment_path cp on (c.id = cp.descendant)
where cp.ancestor = 4 and depth = 1;
-- 查詢4號的所有子評論
select c.* from comment c join comment_path cp on (c.id = cp.descendant) where cp.ancestor = 4;
複製程式碼
Nested Sets(巢狀集)
巢狀集解決方案將資訊儲存在每個節點中,每個節點對應於其後代的集合,而不是節點的直接父節點。
在巢狀集設計中,樹的操作、插入和移動節點通常比在其他模型中更加複雜。
插入新節點時,需要重新計算所有大於新節點左值的左、右值。
巢狀集過於複雜,本人也沒完全搞懂,本文不做詳細介紹。可以參考MySQL實現巢狀集合模型
Path enumeration (路徑列舉)
該方式以專案中組織表為例,與路徑列舉的區別是這裡採用org_num來代替path,
專案中我們約定每一層級的編號長度為2,最大層級為20,每層的子節點數99,最大可儲存節點數為: 2的19次方個節點
層級結構如下:
操作資料的幾種方式
約定 orgNum某節點的編號, orgLen 某節點編號的長度
- 拿到某節點的某幾層子節點
-- 在java程式碼中根據當前節點orgNum得到 orgLen和maxLength
select * from orgs where org_num like '#{orgNum}%' and length(org_num) > #{orgLen} and length(org_num) <=#{maxLength}
複製程式碼
- 拿到某個節點的所有子節點
select * from orgs where org_num like '#{orgNum}%' and length(org_num) > #{orgLen}
複製程式碼
- 獲取父節點或向上找幾層的父節點
-- 先在java程式碼中的根據當前節點orgNum,計算得出要找節點的orgNum,
select * from orgs where org_num = #{orgNum}
複製程式碼
- 刪除組織結構
-- 刪除所有子節點,和查詢所有子節點型別
delete from orgs where org_num like '#{orgNum}%' and length(org_num) > #{orgLen}
複製程式碼
- 節點移動
// 節點移動,更新當前節點的org_num以及所有下級子節點的org_num
// 如果牽扯到節點排序,則除了更新當前節點的orgNum外,還要考慮要移動到的同級節點的orders
複製程式碼
- 組織樹構造
// 先從資料庫中得到某個節點本身rootOrg及所有下級節點OrgList
private OrgTreeDTO orgTreeGenerate(List<Org> OrgList,Org rootNode,Org parent) {
List<Org> subNodes = OrgList.stream().filter(x ->
x.getOrgNum().startsWith(rootNode.getOrgNum())
&& x.getOrgNum().length() == rootNode.getOrgNum().length() + OrgConst.ORG_NODE_LENGTH
&& !x.getId().equals(rootNode.getId())
).collect(Collectors.toList());
OrgTreeDTO orgTreeDTO = new OrgTreeDTO().setId(rootNode.getId())
.setName(rootNode.getName())
.setOrders(rootNode.getOrders())
.setType(rootNode.getType())
.setChildren(subNodes.stream().map(x ->
orgTreeGenerate(OrgList,x,rootNode)
).collect(Collectors.toList()));
if(!ObjectUtils.isEmpty(parent)){
orgTreeDTO.setParentId(parent.getId())
.setParentName(parent.getName());
}
return orgTreeDTO;
}
複製程式碼
以上幾種方法幾乎可以滿足大部分的組織結構調整需求。
該種方式的優點是可以通過簡單查詢從資料庫中得到想要的資料,具體組裝放在java程式碼中處理。
缺點如果層級過深,org_num
會越來越長影響查詢效率,所以專案中限制節點深度為20,可以滿足幾乎所有的組織結構資料。
總結
本文到此結束,如果類似設計上的想法或疑問,歡迎討論。
理論明確實踐的方向,實踐鑑定理論的真偽。