目錄
1 基礎數據
我們以以下數據為例進行說明
graph TD; A --> AA; A --> AB; A --> AC; AB --> ABA; AB --> ABB; AB --> ABC; AC --> ACA; ACA --> ACAA; ACA --> ACAB;
2 繼承關系驅動的架構設計
2.1 表結構
id | parent_id | name |
---|---|---|
1 | A | |
2 | 1 | AA |
3 | 1 | AB |
4 | 3 | ABA |
5 | 3 | ABB |
6 | 3 | ABC |
7 | 1 | AC |
8 | 7 | ACA |
9 | 8 | ACAA |
10 | 8 | ACAB |
2.2 方案的優點及缺點
- 優點: 設計和實現簡單, 直觀
- 缺點: CURD操作是低效的, 主要歸根於頻繁的“遞歸”操作導致的IO開銷
- 解決方案: 在數據規模較小的情況下可以通過緩存機制來優化
3 基於左右值編碼的架構設計
關於此方案的設計可以查看另一篇博客, 本人也是通過查看此篇博客學習的, 一些說明也是直接粘過來的, 所以部分細節我這里不再說明, 本篇博客與其的區別主要在於第四節
在基於數據庫的一般應用中,查詢的需求總要大於刪除和修改。為了避免對於樹形結構查詢時的“遞歸”過程,基於Tree的前序遍歷設計一種全新的無遞歸查詢、無限分組的左右值編碼方案,來保存該樹的數據。
3.1 表結構
id | left | right | name |
---|---|---|---|
1 | 1 | 20 | A |
2 | 2 | 3 | AA |
3 | 4 | 11 | AB |
4 | 5 | 6 | ABA |
5 | 7 | 8 | ABB |
6 | 9 | 10 | ABC |
7 | 12 | 19 | AC |
8 | 13 | 18 | ACA |
9 | 14 | 15 | ACAA |
10 | 16 | 17 | ACAB |
第一次看見這種表結構,相信大部分人都不清楚左值(left)和右值(right)是如何計算出來的,而且這種表設計似乎並沒有保存父子節點的繼承關系。但當你用手指指着表中的數字從1數到20,你應該會發現點什么吧。對,你手指移動的順序就是對這棵樹進行前序遍歷的順序,如下圖所示。當我們從根節點A左側開始,標記為1,並沿前序遍歷的方向,依次在遍歷的路徑上標注數字,最后我們回到了根節點A,並在右邊寫上了20。
graph TD; A["(1) A (20)"] --> AA["(2) AA (3)"]; A --> AB["(4) AB (11)"]; AB --> ABA["(5) ABA (6)"]; AB --> ABB["(7) ABB (8)"]; AB --> ABC["(9) ABC (10)"]; A --> AC["(12) AC (19)"]; AC --> ACA["(13) AC (18)"]; ACA --> ACAA["(14) AC (15)"]; ACA --> ACAB["(16) AC (17)"];
3.2 方案優缺點
- 優點:
- 可以方便的查詢出某個節點的所有子孫節點
- 可以方便的獲取某個節點的族譜路徑(即所有的上級節點)
- 可已通過自身的left, right值計算出共有多少個子孫節點
- 缺點:
- 增刪及移動節點操作比較復雜
- 無法簡單的獲取某個節點的子節點
4 基於繼承關系及左右值編碼的架構設計
其實就是在第三節的基礎上又加了一列parent_id, 目的是在保留上述優點的同時可以簡單的獲取某個節點的直屬子節點
4.1 表結構
id | parent_id | left | right | name |
---|---|---|---|---|
1 | 1 | 20 | A | |
2 | 1 | 2 | 3 | AA |
3 | 1 | 4 | 11 | AB |
4 | 3 | 5 | 6 | ABA |
5 | 3 | 7 | 8 | ABB |
6 | 3 | 9 | 10 | ABC |
7 | 1 | 12 | 19 | AC |
8 | 7 | 13 | 18 | ACA |
9 | 8 | 14 | 15 | ACAA |
10 | 8 | 16 | 17 | ACAB |
4.2 CURD操作
4.2.1 create node
# 為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_ and `is_delete` = 0) 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
# 創建根節點
call tree_create_node(0, 'A')
# 為節點1創建名為AB的子節點
call tree_create_node(1, 'AB')
4.2.2 delete node
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的子孫節點也會被刪除
call tree_delete_node(2)
4.2.3 move node
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 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=???)
# 根據節點id獲取此節點的所有上級節點(包括自己)
select * from tree_table where
left <= (select left from tree_table where id=???) and
right >= (select right from tree_table where id=???)
5 總結
此篇文章對左右值編碼結構的原理介紹的不多, 需要詳細了解的可以查閱末尾引用的博客