-
引言
數據庫應用中常須要在一個有序數據子集中,對指定的若干條記錄進行上下移動。例如,管理員須要對新聞列表中的若干條新聞置頂,考試出卷時須要對選定題目進行上下移動重排順序,等等。sql
總的應該場景在數據表中能夠歸納爲以下模型:數據庫
數據表 TblData(id,fid,rank),id表示記錄的惟一標識,fid指記錄的父節點,rank表明父節點下兄弟的先後順序,依次從1遞增,沒有空隙。oop
問題是要對相同fid下選中的若干個節點進行上下移動,如圖1中的2個示例:3d

圖 1 上移操做示例圖code
左側示例是對第五、6兩個連續的記錄上移,右側示例是對第4和6個有間隔的記錄上移。上移後,要移動的記錄間的間隔保持不變。現實中,選中記錄的位置和數量都是隨機的,移動方向有「上移」1位,「下移」1位,「置頂」和「置底」。該需求進入存儲過程的參數可歸結爲 PROCEDURE Reorder(OUT out_errno INT, IN in_ids TEXT, IN in_n INT UNSIGNED, IN in_direction),其中,out_errno 用於返回狀態碼,in_ids 是要移動的多個 id,以「,」分隔;in_n 是要移動的 id 數量;in_direction 是移動方向,取值是"up","down","top","bottom"。blog
-
上移操做
以塊爲單位的上移
以向上移動(上移或置頂)爲例(如圖2所示),將連續的記錄看做一個塊,按升序依次對各塊進行上移操做。示例中有3個塊,所以須要移動3次。pdo

圖2 上移操做塊示意圖io
特定塊的上移
對具體的塊的移動,須要肯定移動的步長(step)和塊中的記錄數(size)。經過第1塊能夠肯定移動的步長,而且該步長對全部塊都適用,即,全部塊移動的步長都和第1塊相同。圖2中,若是進行「上移」,則移動的步長是 step = 1;若是進行「置頂」,則移動的步長是 step = 4。不一樣塊中的記錄數可能不一樣。圖2中第1塊的記錄是5和6,所以記錄數是 size = 2。class
以塊1的「置頂」操做爲例(如圖3所示)。以要移動的記錄5做爲基準點,current=5,對其上面步長範圍內的記錄下移,並對其下面塊中記錄數範圍內的記錄上移,即,對[current-step,current-1]範圍的記錄做下移 (+size),對[current,current+size-1]範圍的記錄上移 (-step)。效率

圖3 特定塊的上移示意圖
該過程經過1條SQL語句能夠實現:
UPDATE TblData
SET rank=rank+IF(rank<current,size,-step)
WHERE fid=l_fid -- 其中,l_fid 是指記錄共同的父節點編號
AND rank BETWEEN current-step AND current+size-1
-
下移操做
下移過程和上移過程相似,區別是移動要從下面的塊開始(即,編號大的塊),與下面的記錄進行交換(如圖4所示)。

圖4 特定塊的下移示意圖
-
以塊爲操做單位的可選方案
從前文能夠看出,肯定移動步長(step)和塊內記錄數(size)起到關鍵做用。 移動步長根據第1塊(上移操做)或最後1塊(下移操做)很容易肯定。 但對於塊內記錄數卻不那麼容易,須要設置遊標判斷相鄰兩條記錄是否連續,是否有間隙,還要進行記數,並在存在間隙時須要暫停,轉去執行具體塊的移動操做。 可選方案是對要移動的記錄不做分塊處理,即每條要移動的記錄都看做一個塊進行移動。 這樣帶來的後果是:
- 圖2示例中,若按塊移動,則需移3次(由於有3個塊須要上移);若不分塊移動,則需移動4次(由於有4條記錄須要上移)。移動次數取決於塊數和記錄數的區別。
- 圖2示例中,若按塊移動,則須要肯定每1塊的記錄數(第1塊是2,第2和3塊都是1);若不分塊移動,則不須要肯定每塊記錄數,或能夠當作每塊記錄數都是1。是否省去了統計塊內記錄數的區別。
- 效率上,主要取決於塊數和記錄數;邏輯上,後者更簡單清晰。所以,本人傾向後者。
-
完整實現代碼
使用MySQL存儲過程實現上述功能,參考代碼以下。假設傳入參數在數據庫外的合法性已經保證,數據庫內的合法性須要驗證;假設要移動的記錄數大於0;假設數據表中記錄都是有序連續的。
DROP PROCEDURE IF EXISTS Reorder; CREATE PROCEDURE Reorder( OUT out_errno INT, IN in_ids TEXT, IN in_n INT UNSIGNED, IN in_direction VARCHAR(16) ) -- block0 是整個程序最外層的範圍,其中還包含 block1(檢測合法性) 和 block2(實際移動操做) block0: BEGIN DECLARE l_rank_max, -- 用於記錄最大Rank值,也即記錄總數 l_fid -- 用於記錄父節點編號 INT UNSIGNED; -- block1 對傳入參數在數據庫端合法性進行檢測 block1: BEGIN -- 將要移動的記錄存入臨時表 t0(id,rank,processed) -- 其中,processed用於block2中循環處理待移動記錄的標記 IF TRUE THEN DROP TEMPORARY TABLE IF EXISTS t0; SET @sql = CONCAT(' CREATE TEMPORARY TABLE t0 SELECT id, rank,'no' processed FROM TblData WHERE id IN(',in_ids,') ORDER BY rank ',IF(in_direction IN('up','top'),'ASC','DESC'),' -- 要移動的記錄,若上移,則按rank升序排列,不然降序 '); PREPARE s FROM @sql;EXECUTE PREPARE s;DEALLOCATE PREPARE s; END IF; -- 101 檢測要移動的記錄是否都存在 IF (SELECT COUNT(id) FROM t0)<>in_n THEN SET out_errno = 101; LEAVE block0; END IF; -- 102 要移動的記錄都隸屬於同一父節點 IF ( SELECT COUNT(DISTINCT fid) FROM t0 )>1 THEN SET out_errno = 102; LEAVE block0; END IF; SET l_fid = (SELECT fid FROM t0 LIMIT 1); SET l_rank_max = (SELECT MAX(rank) FROM TblData WHERE fid=l_fid); IF in_direction IN('up','top') THEN -- 103 若是上移,是否已經在頂端 IF ( SELECT rank=1 FROM t0 LIMIT 1 ) THEN SET out_errno = 103; LEAVE block0; END IF; ELSE -- 104 若是下移,是否已經在底端 IF ( SELECT rank=l_rank_max FROM t0 LIMIT 1 ) THEN SET out_errno = 104; LEAVE block0; END IF; END IF; END block1; block2: BEGIN DECLARE l_step, l_size, l_current INT UNSIGNED; SET l_size = 1; IF in_direction IN('up','top') THEN -- 上移操做 SET l_step = IF( in_direction='up', 1, (SELECT rank-1 FROM t0 LIMIT 1) ); myloopup: LOOP -- 全部要移動的記錄都已經處理完 IF NOT ( SELECT id FROM t0 WHERE processed='no' LIMIT 1 ) THEN LEAVE myloopup; END IF; -- 對要移動的記錄 t0.processed='no' 的記錄進行上移 SET l_current = (SELECT rank FROM t0 WHERE processed='no' LIMIT 1); UPDATE TblData SET rank=IF(rank<l_current,+l_size,-l_step) WHERE fid=l_fid AND rank BETWEEN l_current-l_step AND l_current+l_size-1; -- 設置標記位,表示已經處理過 UPDATE t0 SET processed='yes' WHERE processed='no' LIMIT 1; END LOOP myloopup; ELSE -- 下移操做 SET l_step = IF( in_direction='down', 1, (SELECT l_rank_max-rank FROM t0 LIMIT 1) ); myloopdown: LOOP IF NOT( SELECT id FROM t0 WHERE processed='no' LIMIT 1 ) THEN LEAVE myloopdown; END IF; SET l_current = (SELECT rank FROM t0 WHERE processed='no' LIMIT 1); UPDATE TblData SET rank=rank+IF(rank>=l_current,-l_size,+l_step) WHERE fid=l_fid AND rank BETWEEN l_current-l_size+1 AND l_current+l_step; -- 設置標記位,表示已經處理過 UPDATE t0 SET processed='yes' WHERE processed='no' LIMIT 1; END loop myloopdown; END IF; SET out_errno = 0; -- 表示移動完成 END block2; END block0
上述源代碼中,上移和下移的過程能夠合併,但展現的邏輯會略複雜,相對難理解。讀者若但願合併,可自行改寫代碼。