mysql無限級分類實現基於彙報關係的信息管理權限

   彙報關係和家族族譜的實現類似,採用樹的數據結構進行定義,樹採用遞歸進行定義。即要嘛是一個根節點,要嘛是由一個根節點和其子樹組成。OA中的彙報關係也採用這種結構(與樹稍有不同),除董事長外,其他人有且只有一個非其本人的直接主管,董事長的直接主管和越級主管是其本人。從以上的定義其實可以看出,彙報關係類似樹,但又與樹並不完全相同。除董事長外,其他彙報關係均是樹形結構。樹形結構採用遞歸定義,如採用遞歸查詢是非常耗時的操作。比如以下需求:

   1.主管可以看到所有直線下屬的績效信息;

   針對以上需求,我們提出三種方法

   1.遞歸:如果採用樹遞歸進行查詢,對於龐大的數據量和較深的層級來說是比較耗時的。

   2.父類id前綴:即父id爲fid,子類編碼規則則是以fid爲前綴,查詢的時候採用like,這種方法的缺點是1.不符合數據庫規範要求;2.修改和移動耗時或不便

   以下采用一種mysql無限級分類的方法來實現基於彙報關係的信息管理權限。

   先了解一下mysql的無限級分類實現方法——改進前序遍歷樹

   以下圖爲例,先把樹按照水平方式擺開。從根節點開始(“Food”),然後他的左邊寫上1。然後按照樹的順序(從上到下)給“Fruit”的左邊寫上2。這樣,你沿着樹的邊界(這就是“遍歷”),然後同時在每個節點的左邊和右邊寫上數字。最後,我們回到了根節點“Food”在右邊寫上18。下面是標上了數字的樹,同時把遍歷的順序用箭頭標出來了。 
wKioL1en_DyQu5BIAACgrlvf8ck605.jpg-wh_50   

   我們稱這些數字爲左值和右值(如,“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

   生成的數據格式如下:

wKiom1eoImGiWhvRAABn_VCDlts549.png-wh_50

wKiom1eoImGwcn_tAAB4YmomwXM685.png-wh_50

   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

   生成的改進前序遍歷樹數據如下:

wKioL1eoI2TRxXEpAABzfgoFR9I911.png-wh_50

   前序遍歷樹數據生成以後,在需要使用以彙報關係做爲基準的數據範圍查詢中增加如下代碼:

	<!-- 查詢當前工號的彙報關係下屬 -->
	<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.刪除

   刪除的原理:

  1. 得到要刪除節點的左右值,並得到他們的差再加一,@mywidth = @rgt - @lft + 1; 

  2. 刪除左右值在本節點之間的節點 

  3. 修改條件爲大於本節點右值的所有節點,操作爲把他們的左右值都減去@mywidth 

  4. 只允許刪除葉子節點,如需刪除葉子節點,先移動該節點不葉子節點,然後刪除


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存儲過程實現的無限級分類,前序遍歷樹(本文的代碼主要參考該文章)

SQL Server存儲層級數據實現無限級分類

一個帶存儲過程的無限級分類數據庫設計  

[原創]另類非遞歸的無限級分類(存儲過程版)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章