樹形結構的數據庫表設計(3種方式)
1 基礎數據
2 繼承關系驅動的架構設計
3 基於左右值編碼的架構設計
4 基於繼承關系及左右值編碼的架構設計
1、基礎數據
我們以以下數據為例進行說明

2、繼承關系驅動的架構設計
2.1表結構

2.2方案的優點及缺點
優點: 設計和實現簡單, 直觀
缺點: CURD操作是低效的, 主要歸根於頻繁的“遞歸”操作導致的IO開銷
解決方案: 在數據規模較小的情況下可以通過緩存機制來優化
3、基於左右值編碼的架構設計
3.1表結構

第一次看見這種表結構,相信大部分人都不清楚左值(left)和右值(right)是如何計算出來的,而且這種表設計似乎並沒有保存父子節點的繼承關系。但當你用手指指着表中的數字從1數到20,你應該會發現點什么吧。對,你手指移動的順序就是對這棵樹進行前序遍歷的順序,如下圖所示。當我們從根節點A左側開始,標記為1,並沿前序遍歷的方向,依次在遍歷的路徑上標注數字,最后我們回到了根節點A,並在右邊寫上了20。

3.2 方案優缺點
優點:
可以方便的查詢出某個節點的所有子孫節點
可以方便的獲取某個節點的族譜路徑(即所有的上級節點)
可已通過自身的left, right值計算出共有多少個子孫節點
缺點:
增刪及移動節點操作比較復雜
無法簡單的獲取某個節點的子節點
4、基於繼承關系及左右編碼的架構設計
其實就是在第三節的基礎上又加了一列parent_id, 目的是在保留上述優點的同時可以簡單的獲取某個節點的直屬子節點
4.1表結構

4.2CURD操作
4.2.1 create note(創建節點)
# 為id為 id_ 的節點創建名為 name_ 的子節點 CREATE PROCEDURE `tree_create_node`(IN `id_` INT, IN `name_` VARCHAR(50)) LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER COMMENT '創建節點' BEGIN declare right1 int; # 當 id_ 為 0 時表示創建根節點 if id_ = 0 then # 此處我限制了僅允許存在一個根節點, 當然這並不是必須的 if exists(select `id` from tree_table where `left` = 1) then SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '根節點已存在'; end if; insert into tree_table(`parent_id`, `name`, `left`, `right`) values(0, name_, 1, 2); commit; elseif exists(select `id` from tree_table where `parent_id` = id_ and `name` = name_) then # 禁止在同一級創建同名節點 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已存在同名兄弟節點'; elseif exists(select `id` from tree_table where `id` = id_) then start transaction; set right1=(select `right` from tree_table where `id` = id_); update tree_table set `right` = `right` + 2 where `right` >= right1; update tree_table set `left` = `left` + 2 where `left` >= right1; insert into tree_table(`parent_id`, `name`, `left`, `right`) values(id_, name_, right1, right1 + 1); commit; # 下面一行僅為了展示以下新插入記錄的id, 並不是必須的 select LAST_INSERT_ID(); else SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '父節點不存在(未創建或被刪除)'; end if; END
# 創建根節點(參數:0代表parentId,'A'代表name) call tree_create_node(0, 'A') # 為節點1創建名為AB的子節點 call tree_create_node(1, 'AB')
4.2.2 delete note(刪除節點)
CREATE PROCEDURE `tree_delete_node`(IN `id_` INT) LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER COMMENT '' BEGIN declare left1 int; declare right1 int; if exists(select id from tree_table where id = id_) then start transaction; select `left`, `right` into left1, right1 from tree_table where id = id_; delete from tree_table where `left` >= left1 and `right` <= right1; update tree_table set `left` = `left` - (right1-left1+1) where `left` > left1; update tree_table set `right` = `right` - (right1-left1+1) where `right` > right1; commit; end if; END
# 刪除節點2, 節點2的子孫節點也會被刪除(參數:2代表當前節點id)
call tree_delete_node(2)
4.2.3 move note(移動節點)
move的原理是先刪除再添加, 但涉及被移動的節點的left, right值不能亂所以需要使用臨時表(由於在存儲過程中無法創建臨時表, 此處我使用了一張正常的表進行緩存, 歡迎提出更合理的方案)
# 此存儲過程中涉及到is_delete字段, 表示數據是否被刪除, 因為正式環境中刪除操作一般都不會真的刪除而是進行軟刪(即標記刪除), 如果不需要此字段請自行對程序進行調整 CREATE PROCEDURE `tree_move_node`(IN `self_id` INT, IN `parent_id` INT , IN `sibling_id` INT) LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER COMMENT '' BEGIN declare self_left int; declare self_right int; declare parent_left int; declare parent_right int; declare sibling_left int; declare sibling_right int; declare sibling_parent_id int; if exists(select id from tree_table where id = parent_id and is_delete = 0) then # 創建中間表 CREATE TABLE If Not Exists tree_table_self_ids (`id` int(10) unsigned NOT NULL); truncate tree_table_self_ids; start transaction; # 事務 # 獲取移動對象的 left, right 值 select `left`, `right` into self_left, self_right from tree_table where id = self_id; # 將需要移動的記錄的 id 存入臨時表, 以保證操作 left, right 值變化時這些記錄不受影響 insert into tree_table_self_ids(id) select id from tree_table where `left` >= self_left and `right` <= self_right; # 將被移動記錄后面的記錄往前移, 填充空缺位置 update tree_table set `left` = `left` - (self_right-self_left+1) where `left` > self_left and id not in (select id from tree_table_self_ids); update tree_table set `right` = `right` - (self_right-self_left+1) where `right` > self_right and id not in (select id from tree_table_self_ids); select `left`, `right` into parent_left, parent_right from tree_table where id = parent_id; if sibling_id = -1 then # 在末尾插入子節點 update tree_table set `right` = `right` + (self_right-self_left+1) where `right` >= parent_right and id not in (select id from tree_table_self_ids); update tree_table set `left` = `left` + (self_right-self_left+1) where `left` >= parent_right and id not in (select id from tree_table_self_ids); update tree_table set `right`=`right` + (parent_right-self_left), `left`=`left` + (parent_right-self_left) where id in (select id from tree_table_self_ids); elseif sibling_id = 0 then # 在開頭插入子節點 update tree_table set `right` = `right` + (self_right-self_left+1) where `right` > parent_left and id not in (select id from tree_table_self_ids); update tree_table set `left` = `left` + (self_right-self_left+1) where `left` > parent_left and id not in (select id from tree_table_self_ids); update tree_table set `right`=`right` - (self_left-parent_left-1), `left`=`left` - (self_left-parent_left-1) where id in (select id from tree_table_self_ids); else # 插入指定節點之后 select `left`, `right`, `parent_id` into sibling_left, sibling_right, sibling_parent_id from tree_table where id = sibling_id; if parent_id != sibling_parent_id then SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '指定的兄弟節點不在指定的父節點中'; end if; update tree_table set `right` = `right` + (self_right-self_left+1) where `right` > sibling_right and id not in (select id from ctree_table_self_ids); update tree_table set `left` = `left` + (self_right-self_left+1) where `left` > sibling_right and id not in (select id from tree_table_self_ids); update tree_table set `right`=`right` - (self_left-sibling_right-1), `left`=`left` - (self_left-sibling_right-1) where id in (select id from tree_table_self_ids); end if; update tree_table set `parent_id`=parent_id where `id` = self_id; commit; else SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '父節點不存在(未創建或被刪除)'; end if; END
# 將節點2移動到節點1下面開頭的位置 call tree_move_node(2, 1, 0) # 將節點2移動到節點1下面末尾的位置 call tree_move_node(2, 1, -1) # 將節點2移動到節點1下面且跟在節點3后面的位置 call tree_move_node(2, 1, 3)
4.2.4 select(查詢節點以及子節點)
# 以下sql中需要傳的值全用???表示(正常寫代碼使用#{}傳值) # 根據節點id獲取此節點所有子孫節點 select * from tree_table where `left` > (select `left` from tree_table where id=???) and `right` < (select `right` from tree_table where id=???) # 根據節點id獲取此節點的所有子孫節點(包含自己) select * from tree_table where `left` >= (select `left` from tree_table where id=???) and `right` <= (select `righ`t from tree_table where id=???) # 根據節點id獲取此節點的所有上級節點 select * from tree_table where `left` < (select `left` from tree_table where id=???) and `right` > (select `right` from tree_table where id=???) # 根據節點id獲取此節點的所有上級節點(包括自己) select * from tree_table where `left` <= (select `left` from tree_table where id=???) and `right` >= (select `right` from tree_table where id=???)
注意:上述字段設計使用到了left和right,這屬於關鍵字,在寫sql語句時需要加上``符號。
5、總結
上述多數內容屬於函數的存儲過程,只需要在數據庫表建好之后執行即可。正常添加、刪除、移動節點只需要執行call.....這個函數即可。
代碼編寫中,可以如下:
/** * 添加節點內容 * @param parentId * @param name */ @Insert("call tree_create_node(#{parentId}, #{name})") void saveStructureTree(Integer parentId, String name); /** * 刪除當前節點以及子節點信息 * @param id */ @Delete("call tree_delete_node(#{id})") void deleteStructureTree(@Param("id")Integer id);
本文轉自:https://blog.csdn.net/p1049990866/article/details/95760835,稍作修改。
