1. 程式人生 > 程式設計 >在關係型資料庫中優雅的處理組織架構樹

在關係型資料庫中優雅的處理組織架構樹

我們在專案開發中經常會遇到這樣的場景:企業組織架構,回覆評論鏈,商品類目等樹型結構。本文結合網上一些資料,以及自己在專案中的實踐來說一說幾種處理樹型結構的方式。

可能有不少小夥伴遇到該問題,第一個想法就是我在每條資料上都儲存它的parent_id就可以滿足需求了啊,這樣一層一層都能找到, 寫一個遞迴演演算法就可以拿到他的所有下級或者上級。這是一種可以實現的方式,我們先不評價這種方式的優劣。

下面先說下幾種常見的處理方式:

  • Adjacency list (鄰接表)
  • Closure table (閉包表)
  • Path enumeration (路徑列舉)
  • Nested Sets(巢狀集)

本文主要講解Path enumeration (路徑列舉),其它幾種只做簡單的展示,有需要了解的可以點選一下連線檢視

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,可以滿足幾乎所有的組織結構資料。

總結

本文到此結束,如果類似設計上的想法或疑問,歡迎討論。

團隊部落格連結,歡迎點選

理論明確實踐的方向,實踐鑑定理論的真偽。