彙報關係和家族族譜的實現類似,採用樹的數據結構進行定義,樹採用遞歸進行定義。即要嘛是一個根節點,要嘛是由一個根節點和其子樹組成。OA中的彙報關係也採用這種結構(與樹稍有不同),除董事長外,其他人有且只有一個非其本人的直接主管,董事長的直接主管和越級主管是其本人。從以上的定義其實可以看出,彙報關係類似樹,但又與樹並不完全相同。除董事長外,其他彙報關係均是樹形結構。樹形結構採用遞歸定義,如採用遞歸查詢是非常耗時的操作。比如以下需求:
1.主管可以看到所有直線下屬的績效信息;
針對以上需求,我們提出三種方法
1.遞歸:如果採用樹遞歸進行查詢,對於龐大的數據量和較深的層級來說是比較耗時的。
2.父類id前綴:即父id爲fid,子類編碼規則則是以fid爲前綴,查詢的時候採用like,這種方法的缺點是1.不符合數據庫規範要求;2.修改和移動耗時或不便
以下采用一種mysql無限級分類的方法來實現基於彙報關係的信息管理權限。
先了解一下mysql的無限級分類實現方法——改進前序遍歷樹
以下圖爲例,先把樹按照水平方式擺開。從根節點開始(“Food”),然後他的左邊寫上1。然後按照樹的順序(從上到下)給“Fruit”的左邊寫上2。這樣,你沿着樹的邊界(這就是“遍歷”),然後同時在每個節點的左邊和右邊寫上數字。最後,我們回到了根節點“Food”在右邊寫上18。下面是標上了數字的樹,同時把遍歷的順序用箭頭標出來了。
我們稱這些數字爲左值和右值(如,“Food”的左值是1,右值是18)。正如你所見,這些數字按時了每個節點之間的關係。因爲“Red”有3和6兩個值,所以,它是有擁有1-18值的“Food”節點的後續。同樣的,我們可以推斷所有左值大於2並且右值小於11的節點,都是有2-11的“Fruit” 節點的後續。這樣,樹的結構就通過左值和右值儲存下來了。這種數遍整棵樹算節點的方法叫做“改進前序遍歷樹”算法。
該算法具有據存儲冗餘小、直觀性強;方便返回整個樹型結構數據;可以很輕鬆的返回某一子樹(方便分層加載);快整獲以某節點的祖譜路徑;插入、刪除、移動節點效率高等特點。
我們需要解決的最集中問題是查詢效率。從圖上看,我們如果查詢Fruit的所有子類,只需要查詢其左值>父結點左值&&右值小於父節點右值的節點。其查詢時間複雜度是常數階O(1),而最有利的子樹查詢算法也是對數階O(lgn)。從時間複雜度角度該算法的查詢效率是達到最優的。
在本方法中主要採用兩張表來實現,一張是彙報關係生成方法2格式的彙報關係對照表,第二張用於存儲生成的改進前序遍歷樹
表1 tmpList定義如下:
SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for tmplst -- ---------------------------- DROP TABLE IF EXISTS `tmplst`; CREATE TABLE `tmplst` ( `id` varchar(50) DEFAULT NULL, `pid` varchar(50) DEFAULT NULL, `leapfrogLeaderCode` varchar(50) DEFAULT NULL, `nLevel` int(11) DEFAULT NULL, `sCort` varchar(8000) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表2 tree_node定義如下:
SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for tree_node -- ---------------------------- DROP TABLE IF EXISTS `tree_node`; CREATE TABLE `tree_node` ( `id` varchar(20) NOT NULL DEFAULT '' COMMENT '工號', `pid` varchar(20) DEFAULT NULL COMMENT '直接主管工號', `leapfrogLeaderCode` varchar(20) DEFAULT NULL COMMENT '越級主管工號', `leftId` int(50) DEFAULT NULL COMMENT '前序遍歷樹右節點左節點', `rightId` int(50) DEFAULT NULL COMMENT '前序遍歷樹右節點', `treeid` varchar(50) NOT NULL DEFAULT 'hbgx', PRIMARY KEY (`id`), KEY `idx_tree_node_leftId` (`leftId`) USING BTREE, KEY `idx_tree_node_rightId` (`rightId`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
由於彙報關係並不是簡單的樹形結構,第一個節點即根節點是特殊的。所以這個節點作爲特殊節點單獨插入,採用如下存儲過程:
ExitLabel:BEGIN DECLARE vLeftId INT; DECLARE aff INT; SELECT `leftId` INTO vLeftId FROM `tree_node` WHERE `id`= rootid AND `pid` = 0; IF vLeftId IS NOT NULL THEN LEAVE ExitLabel; END IF; START TRANSACTION; INSERT INTO `tree_node`(`id`,`pid`,`leftId`,`rightId`,`leapfrogLeaderCode`) VALUES (rootid,0,1,2,rootid); SELECT ROW_COUNT() INTO aff; IF aff = 1 THEN COMMIT; ELSE ROLLBACK; END IF; END
生成tmpList數據
BEGIN DECLARE Level int ; DECLARE pid VARCHAR(50); DECLARE id VARCHAR(50); DECLARE leapfrogLeaderCode VARCHAR(50); drop TABLE IF EXISTS tmpLst; CREATE TABLE tmpLst ( id varchar(50), pid varchar(50), leapfrogLeaderCode varchar(50), nLevel int, sCort varchar(8000) ); Set Level=0 ; INSERT into tmpLst SELECT staffId,directLeaderCode,leapfrogLeaderCode,Level,staffId FROM t_per_staffinfo WHERE directLeaderCode=rootid; WHILE ROW_COUNT()>0 DO SET Level=Level+1 ; INSERT into tmpLst SELECT A.staffId,A.directLeaderCode,A.leapfrogLeaderCode,Level,concat(B.sCort,A.staffId) FROM t_per_staffinfo A,tmpLst B WHERE A.directLeaderCode=B.ID AND B.nLevel=Level-1 ; END WHILE; END
生成的數據格式如下:
id是員工工號,pid是直接主管工號,leapfrogLeaderCode是越級主管工號,nLevel是層數,sCort是方法2生成的父節點前綴工號
根據tmpList生成tree_node的存儲過程 :
插入操作很簡單找到其父節點,之後把左值和右值大於父節點左值的節點的左右值加上2,之後再插入本節點,左右值分別爲父節點左值加一和加二,可以用一個存儲過程來操作:
ExitLabel:BEGIN DECLARE vRightId INT; DECLARE vTreeId varchar(50); DECLARE aff INT; DECLARE af INT DEFAULT 0; SELECT `rightId` INTO vRightId FROM `tree_node` WHERE `id`= pid; IF vRightId IS NULL THEN LEAVE ExitLabel; END IF; START TRANSACTION; UPDATE `tree_node` SET `leftId`=`leftId`+2 WHERE `leftId` > vRightId; SELECT ROW_COUNT() INTO aff; SET af = aff+af; UPDATE `tree_node` SET `rightId`=`rightId`+2 WHERE `rightId` >= vRightId; SELECT ROW_COUNT() INTO aff; SET af = aff+af; INSERT INTO `tree_node`(`id`,`pid`,`leftId`,`rightId`,`leapfrogLeaderCode`) VALUES (code,pid,vRightId,vRightId+1,leapfrogLeaderCode); SELECT ROW_COUNT() INTO aff; SET af = aff+af; IF af >= 2 THEN COMMIT; ELSE ROLLBACK; END IF; END
生成的改進前序遍歷樹數據如下:
前序遍歷樹數據生成以後,在需要使用以彙報關係做爲基準的數據範圍查詢中增加如下代碼:
<!-- 查詢當前工號的彙報關係下屬 --> <select id="queryTreeNodeByStaffid" parameterType="String" resultMap="UserResultMap"> SELECT u.* FROM T_SYS_USER u LEFT JOIN tree_node t ON u.staffId = t.id WHERE u.userStatus = 'normal' and <![CDATA[ t.leftId > (SELECT leftId FROM tree_node treenode WHERE treenode.id = #{staffId}) ]]> AND <![CDATA[ t.rightId < (SELECT rightId FROM tree_node treenode WHERE treenode.id = #{staffId}) ]]> </select>
由於彙報關係並不是一成不變的,在彙報關係出現變化時,應該修改tree_node表以適應最新的變化,使基於彙報關係的數據範圍查詢更加準確,以下存儲過程是修改遍歷表的:
1.
ExitLabel:BEGIN call moveTreeNode(nodeId,pid,resultCode,resultMsg); IF resultCode=1000 THEN UPDATE `tree_node` SET `leapfrogLeaderCode` = leapfrogLeaderCode WHERE `id`= nodeId; COMMIT; END IF; END
2.
ExitLabel:BEGIN DECLARE vLeftId INT; DECLARE vRightId INT; DECLARE vTreeId varchar(50); DECLARE vPid varchar(50); DECLARE vTargetLeftId INT; DECLARE vTargetRightId INT; DECLARE vDiff INT; DECLARE vLevelDiff INT; DECLARE vGroupTreeId INT; DECLARE vGroupIdStr varchar(1000); SELECT `leftId`,`rightId`,`treeId`,`pid` INTO vLeftId,vRightId,vTreeId,vPid FROM `tree_node` WHERE `id`= nodeId; IF vLeftId IS NULL THEN SET resultCode = 1002; SET resultMsg = "要移動的節點不存在"; LEAVE ExitLabel; END IF; IF vLeftId=1 THEN SET resultCode = 1003; SET resultMsg = "根節點不能移動"; LEAVE ExitLabel; END IF; IF nodeId = targetId THEN SET resultCode = 1004; SET resultMsg = "不能移動到自己"; LEAVE ExitLabel; END IF; IF vPid = targetId THEN SET resultCode = 1000; SET resultMsg = "目標節點是要移動節點的父節點,不需要移動"; LEAVE ExitLabel; END IF; SELECT `leftId`,`rightId` INTO vTargetLeftId,vTargetRightId FROM `tree_node` WHERE `treeId` = vTreeId AND `id`= targetId; IF vTargetLeftId IS NULL THEN SET resultCode = 1006; SET resultMsg = "目標節點不存在"; LEAVE ExitLabel; END IF; IF vTargetLeftId > vLeftId AND vTargetLeftId < vRightId THEN SET resultCode = 1007; SET resultMsg = "目標節點不能是要移動節點的子節點"; LEAVE ExitLabel; END IF; START TRANSACTION; SELECT `treeId`, group_concat(CAST(`id` as char)) as idStr INTO vGroupTreeId,vGroupIdStr FROM `tree_node` WHERE `treeId` = vTreeId AND `leftId` between vLeftId AND vRightId GROUP BY `treeId`; IF vTargetLeftId>vLeftId OR (vTargetLeftId < vLeftId AND vTargetRightId > vRightId) THEN SET vDiff = vTargetRightId - 1 - vRightId; UPDATE `tree_node` SET `leftId`=`leftId` +vDiff, `rightId`=`rightId` + vDiff WHERE `treeId` = vTreeId AND `leftId` between vLeftId AND vRightId; UPDATE `tree_node` SET `pid` = targetId WHERE `id`= nodeId; SET vDiff = vRightId-vLeftId+1; UPDATE `tree_node` SET `leftId`=`leftId`- vDiff WHERE `treeId` = vTreeId AND `leftId`>vRightId AND `leftId`< vTargetRightId AND NOT FIND_IN_SET(CAST(`id` as char),vGroupIdStr); UPDATE `tree_node` SET `rightId`=`rightId`- vDiff WHERE `treeId` = vTreeId AND `rightId`>vRightId AND `rightId`< vTargetRightId AND NOT FIND_IN_SET(CAST(`id` as char),vGroupIdStr); ELSE SET vDiff = vLeftId - vTargetRightId; UPDATE `tree_node` SET `leftId`=`leftId` -vDiff, `rightId`=`rightId` - vDiff WHERE `treeId` = vTreeId AND `leftId` between vLeftId AND vRightId; UPDATE `tree_node` SET `pid` = targetId WHERE `id`= nodeId; SET vDiff = vRightId-vLeftId+1; UPDATE `tree_node` SET `leftId`=`leftId`+ vDiff WHERE `treeId` = vTreeId AND `leftId`>vTargetRightId AND `leftId`< vRightId AND NOT FIND_IN_SET(CAST(`id` as char),vGroupIdStr); UPDATE `tree_node` SET `rightId`=`rightId`+ vDiff WHERE `treeId` = vTreeId AND `rightId`>=vTargetRightId AND `rightId`< vRightId AND NOT FIND_IN_SET(CAST(`id` as char),vGroupIdStr); END IF; COMMIT; SET resultCode = 1000; SET resultMsg = "成功"; END
3.刪除
刪除的原理:
得到要刪除節點的左右值,並得到他們的差再加一,@mywidth = @rgt - @lft + 1;
刪除左右值在本節點之間的節點
修改條件爲大於本節點右值的所有節點,操作爲把他們的左右值都減去@mywidth
只允許刪除葉子節點,如需刪除葉子節點,先移動該節點不葉子節點,然後刪除
ExitLabel:BEGIN DECLARE vLeftId INT; DECLARE vRightId INT; DECLARE aff INT; /*影響記錄條數*/ DECLARE af INT DEFAULT 0; /*影響記錄總條數*/ SELECT `leftId`,`rightId` INTO vLeftId,vRightId FROM `tree_node` WHERE `id` = nodeId; IF vLeftId IS NULL THEN LEAVE ExitLabel; END IF; IF vLeftId=1 THEN LEAVE ExitLabel; END IF; START TRANSACTION; DELETE FROM `tree_node` WHERE `leftId` between vLeftId AND vRightId; SELECT ROW_COUNT() INTO aff; SET af = aff+af; UPDATE `tree_node` SET `leftId`=`leftId`-(vRightId-vLeftId+1) WHERE `treeId` = vTreeId AND `leftId`>vLeftId; SELECT ROW_COUNT() INTO aff; SET af = aff+af; UPDATE `tree_node` SET `rightId`=`rightId`-(vRightId-vLeftId+1) WHERE `rightId`>vLeftId; SELECT ROW_COUNT() INTO aff; SET af = aff+af; IF af >= 2 THEN COMMIT; ELSE ROLLBACK; END IF; END
調用如下存儲過程實現更新和插入
BEGIN DECLARE a VARCHAR(30); DECLARE b VARCHAR(30); DECLARE c VARCHAR(30); DECLARE str VARCHAR(300); DECLARE x int; DECLARE s int default 0; DECLARE cursor_name CURSOR FOR select pid,id,leapfrogLeaderCode from tmpLst; DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET s=1; set str = "--"; OPEN cursor_name; fetch cursor_name into a,b,c; while s <> 1 do set str = concat(str,x); call addTreeNode(a ,b,c); fetch cursor_name into a,b,c; end while; CLOSE cursor_name ; select str; END
總的調用次序
call showTreeNodes_yongyupost2000('FXGG0001'); TRUNCATE tree_node ; call addTreeRootNode('FXGG0001'); call add_test()
由於移動的代碼比較晦澀難懂,我們採用的是tree_node定時trancate,重新生成的方式,週期爲一天一次。
參考資料:
應用mysql數據庫設計無限級分類表
mysql存儲過程實現的無限級分類,前序遍歷樹(本文的代碼主要參考該文章)