[toc]node
咱们以如下数据为例进行说明sql
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 |
关于此方案的设计能够查看另外一篇博客, 本人也是经过查看此篇博客学习的, 一些说明也是直接粘过来的, 因此部分细节我这里再也不说明, 本篇博客与其的区别主要在于第四节 在基于数据库的通常应用中,查询的需求总要大于删除和修改。为了不对于树形结构查询时的“递归”过程,基于Tree的前序遍历设计一种全新的无递归查询、无限分组的左右值编码方案,来保存该树的数据。数据库
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。缓存
其实就是在第三节的基础上又加了一列parent_id, 目的是在保留上述优势的同时能够简单的获取某个节点的直属子节点架构
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 |
# 为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')
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)
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)
# 如下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=???)
此篇文章对左右值编码结构的原理介绍的很少, 须要详细了解的能够查阅末尾引用的博客优化