一、前言
很多情況下二級分類已經不能滿足需要了,而網上可用的多級分類的例子實在是不好找,故有此文。
http://bbs.blueidea.com/viewthread.php?tid=1182243
大家可以先看這個,它介紹了一種超級好的演算法,反正我是看不大懂呀。
二、我們要解決的問題:
1、 分類演算法常常表現為樹的表示和遍曆問題。那麼,請問:如果用資料庫中的一個Table來表達樹型分類,應該有幾個欄位?
2、 如何快速地從這個Table恢複出一棵樹;
3、 如何判斷某個分類是否是另一個分類的子類;
4、 如何尋找某個分類的所有產品;
5、 如何產生分類所在的路徑。
6、 如何新增分類;
三、遞迴實現的優點與缺點
該怎麼實現多級分類呢?
估計首先想到的都是遞迴,實現簡單,在指定節點(就是分類,下同)下添加、修改、刪除節點都不是問題,
而且節點移動實現起來也不是很難,只是要注意移動目的父節點不能是當前節點的父節節點(等於沒移動),也不能是當前節點的子節點(類似於window檔案夾,一個檔案夾是不能移動到自己的字檔案夾裡的)。
但是最愁人的是搜尋指定節點下的東西,怎麼辦?也就是上面的問題3。記住,這是要包括所有子節點的,難道還去遞迴嗎?
四、介紹下我的簡單演算法(是我所用的,不是我發明的)
以常見的商品系統為例。
4.1 表結構
[1]分類表,T_Sort,表結構一所示。其中sortPath儲存的是節點路徑,這是個重點。
[2]商品表,T_Product,表結構二所示。
[center]圖一
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
圖二
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_product.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
[/center]
4.2 演算法簡要說明
[1]parentID儲存的自然是節點的父節點,如果一個節點的parentID=0時,認為它是一級分類。
[2]一個節點的sortPath為它的父節點的sortPath+自己的sortID+","。如sortID=32的節點的父節點是節點21,節點21的sortPath是"0,21,",那麼節點32的sortPath就是"0,21,32,"。有點繞,看圖三清楚啦。可能你想不通為啥最後要多個逗號啊,後面你就明白啦。所有節點的sortPath的左邊兩位都是"0,",因為它們都在根節點下。一個節點的sortPath一定包含在它的子節點的sortPath中。
[center]
圖三
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort2.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
4.3 代碼重點講解。
這裡以我們要實現的功能為例講解。
[1]添加節點
<1>選擇父節點,可以是根節點,或是下級所有節點(最好列出一個樹型菜單讓使用者選擇,別愁,可以實現),其實就是選擇parentID。
<2>如果parentID=0,那麼上級sortPath="0,",如果parentID<>0,那麼到表T_Sort根據parentID取得上級sortPath。
<3>給T_Sort新增記錄,sortPath=上級sortPath +新記錄的sortID +","。
<4>範例代碼見圖4、圖5。其中noRecord,closeRs(),showMsg(),closeConn()都是我定義的Function或Sub,它們的功能都是顧名思義的,我就不說了。注意一下,如果你用MS SQL,代碼略有不同。我也很奇怪MS SQL時,addNew後,這個新的自動編號可以輸出,但是和字元一串連就沒有了。各位如果知道為什麼,還請相告。
[center]
圖四
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
圖五
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort_mssql.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
[2]修改節點
節點的屬性只有一個名字而已,直接update就可以了,就不說了。
[3]刪除節點
<1>選擇節點
<2>如果parentID=0,報錯,根節點不能刪除。
<3>刪除該節點及所有子節點。你可能想是不是很麻煩啊,哈哈,其實我只用了一個SQL語句就搞定啦。
[Copy to clipboard] [ - ]
CODE:
(Access)sql="delete from T_Sort where Instr(sortPath,',"&parentID&",')>0"
[Copy to clipboard] [ - ]
CODE:
(MS SQL)sql="delete from T_Sort where CHARINDEX(',"&parentID&",',sortPath)>0"
本演算法的精華就在這裡啦,仔細想想吧,sortPath最後那個逗號的作用也在這裡啦。
<4>刪除上述所有節點下的商品。同上,表名不同而已。
[Copy to clipboard] [ - ]
CODE:
(Access)sql="delete from T_Product where Instr(sortPath,',"&parentID&",')>0"
[Copy to clipboard] [ - ]
CODE:
(MS SQL)sql="delete from T_Product where CHARINDEX(',"&parentID&",',sortPath)>0"
<5>範例代碼見圖6。MS SQL的代碼就不貼了。
[center]
圖六
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/delsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
[4]移動節點
痛點哦,睜大眼睛仔細看。
<1>選擇要移動的節點parentID,選擇目的節點toParentID(也就是把當前節點放到誰的下面)。
<2>如果parentID=0報錯,根節點不能移動。
<3>如果toParentID=parentID,這是要把自己放到自己下面,報錯。
<4>根據parentID,取得它的sortPath,我們叫它fromPath。
<5>如果toParentID=0,那麼toPath="0,",如果toParentID<>0,取得它的sortPath,叫它toPath。
<6>如果toParentID等於要移動節點的父節點,不需要移動,報錯。判斷方法是看toPath & parentID &","是否等於fromPath。
<7>如果toParentID是要移動節點的子節點,不能移動,報錯。判斷方法是看Instr(toPath,fromPath)是否大於0。
<8>組合要移動節點的新sortPath,也就是newPath=toPath & parentID &","。
<9>更新要移動節點及其所有子節點的sortPath()。如"0,2,3,5,"移動到"0,1,"下,那麼新的sortPath就是"0,1,5,"了(想想,對吧)。而"0,2,3,5,"的所有子節點的左半部分都是"0,2,3,5,",那麼只要把"0,2,3,5,"替換成"0,1,5,"就行了。
[Copy to clipboard] [ - ]
CODE:
(Access)sql="update T_Sort set sortPath='"&newPath&"'+Mid(sortPath,Len('"&fromPath&"')+1) where Instr(sortPath,'"&fromPath&"')>0"
[Copy to clipboard] [ - ]
CODE:
(MS SQL)sql="update T_Sort set sortPath=replace(sortPath,'"&fromPath&"','"&newPath&"') where CHARINDEX[('"&fromPath&"',sortPath)>0"
因為Access好像沒有內建repalce函數,所以麻煩了一些。
<10>更新要移動節點的parentID。直接update就行啦。
<11>商品是跟隨分類走的,所以商品的parentID不用更新,只要更新它的sortPath就行了。語句同<9>,只是表名換成T_Product。
<12>範例代碼見圖7。MS SQL的代碼只有上面2個SQL語句不同,不貼了。
[center]
圖七
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" style="WIDTH: 735px; CURSOR: hand; HEIGHT: 738px" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" height=738 alt="Click here to open new window
CTRL+Mouse wheel to zoom in/out" src="http://mytju.com/temp/tech/movesort.gif" width=716 onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0 resized="true">[/center]
[5]前台分類瀏覽商品
前台一般都不會把所有類別一下子都列出來,都是分級瀏覽的,一層一層的看。我們要做的只是瀏覽一個分類時,把它的下級分類列出來。
<1>取直接子類別,很簡單啦。
[Copy to clipboard] [ - ]
CODE:
sql="select sortID,sortName from T_Sort where parentID="&sortID
就行啦。
<2>一般我們都會顯示一個當前位置,就是分類所在的路徑,怎麼辦呢?難道去遞迴查詢嗎?當然不,我這裡用了一個小技巧。
瀏覽某一個分類的時候,我們會有一個sortID,可以根據它從T_Sort取得sortPath....不說了,大家看示範代碼吧,不懂問我。
範例代碼見圖8。
[center]
圖八
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/browersort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
顯示的時候用
[Copy to clipboard] [ - ]
CODE:
<%for i=1 to UBound(myArray)
response.write "-> <a href='product.asp?sortID="&myArray(i)&"'>"&getValueByID(myArray(i),nameArray)&"</a>"
next%>
就OK了。getValueByID是我寫的一個Function,見後。
<3>顯示該類別及其所有子類別下的所有商品。
[Copy to clipboard] [ - ]
CODE:
sql="select * from T_Product where Instr(sortPath,',"&sortID&",')>0"
如果只顯示該類別下的商品,那麼就用parentID判斷就行了。
[6]前台檢索商品
如果你的檢索表單不包含商品類別,那麼沒有什麼特殊的。如果有商品類別的話,也很簡單,SQL的where條件裡加一個
[Copy to clipboard] [ - ]
CODE:
"and Instr(sortPath,',"&sortID&",')>0"
就行了。
[7]後台商品添加、修改
添加是要選擇所在的分類,這樣就可以得到sortID,並能取得它的sortPath,儲存到商品記錄就行了。
修改類似。
[8]後台商品刪除
和分類不相關,直接根據productID刪除就可。
五、附加資訊
目前還沒有拆分出來的代碼給大家(太麻煩),主要的東西都在上面啦。
師傅領進門,修行在個人啦。
[Copy to clipboard] [ - ]
CODE:
'-----根據ID取得name的Sub-----------------
Function getValueByID(sortID,inArray)
dim i
if NOT IsArray(inArray) then
getValueByID=""
Exit Function
end if
for i=0 to UBound(inArray,2)
if Cstr(sortID)=Cstr(inArray(0,i)) then
getValueByID=inArray(1,i) '返回name
Exit Function
end if
next
getValueByID=""
End Function