這節(jié)我們來(lái)完成菜單的實(shí)現(xiàn)诈闺。一般菜單是系統(tǒng)里面必不可少的一項(xiàng)渴庆,像我們的后臺(tái)管理系統(tǒng),一般左邊都有功能導(dǎo)航菜單雅镊,門(mén)戶(hù)網(wǎng)站襟雷,博客,頁(yè)面頂部也會(huì)有導(dǎo)航菜單仁烹。大部分菜單耸弄,都是可以支持多層級(jí)的,理論上我們可以做成無(wú)限層級(jí)的卓缰。這種菜單模型其實(shí)是一個(gè)樹(shù)形結(jié)構(gòu)模型计呈,或者父子結(jié)構(gòu)模型,之前我們處理的實(shí)體全部都是非樹(shù)形結(jié)構(gòu)的(其實(shí)一般角色是樹(shù)形結(jié)構(gòu)的征唬,筆者前面未了簡(jiǎn)化處理捌显,沒(méi)做成樹(shù)形結(jié)構(gòu)),一般來(lái)說(shuō)总寒,筆者把實(shí)體對(duì)象統(tǒng)分為普通對(duì)象和樹(shù)形對(duì)象扶歪。
1、實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)的方案
總的來(lái)說(shuō)摄闸,實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)的方案有四種:
1.1鄰接表
鄰接表將所有節(jié)點(diǎn)數(shù)據(jù)放在一個(gè)表中善镰,然后使用一個(gè)屬性來(lái)標(biāo)示該節(jié)點(diǎn)的父id妹萨。這種方案簡(jiǎn)單,直觀(guān)炫欺。插入乎完,移動(dòng),刪除效率都比較高品洛,查詢(xún)效率比較低树姨,需要遞歸查詢(xún)(查詢(xún)一個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn),包括子節(jié)點(diǎn)的子節(jié)點(diǎn))毫别。一般在數(shù)據(jù)量小的時(shí)候娃弓,我們可以一次查詢(xún)出所有節(jié)點(diǎn),在內(nèi)存中再將其組裝成樹(shù)岛宦,還可以對(duì)整棵樹(shù)進(jìn)行緩存。數(shù)據(jù)量多的時(shí)候耍缴,我們可以進(jìn)行異步加載砾肺,即每展開(kāi)一個(gè)節(jié)點(diǎn),才去查詢(xún)他的直接子節(jié)點(diǎn)(不需要遞歸查詢(xún))防嗡。
1.2路徑枚舉
為了解決鄰接表遞歸查詢(xún)的問(wèn)題变汪,路徑枚舉在鄰接表的設(shè)計(jì)上,增加了一個(gè)路徑字段蚁趁。
如上圖所示裙盾,paths就是從根節(jié)點(diǎn)id到當(dāng)前節(jié)點(diǎn)id的一個(gè)層次結(jié)構(gòu)。
該方案在插入他嫡,移動(dòng)的時(shí)候效率比較低番官,需要額外維護(hù)一個(gè)paths字段,在插入時(shí)钢属,paths字段的生成徘熔,首先需要獲取其父節(jié)點(diǎn)的paths值,然后再加上自身節(jié)點(diǎn)的id值淆党。在移動(dòng)時(shí)酷师,所有子節(jié)點(diǎn)的paths將會(huì)跟著發(fā)生變化,需要重新計(jì)算染乌。在查詢(xún)?nèi)我夤?jié)點(diǎn)的子節(jié)點(diǎn)(包括非直接子節(jié)點(diǎn))時(shí)山孔,只需一個(gè)sql語(yǔ)句就能查出,如查詢(xún)A節(jié)點(diǎn)所有的子節(jié)點(diǎn)荷憋,只需要加入paths like '1-%'條件即可台颠。
該方案理論上樹(shù)形結(jié)構(gòu)可以不限層級(jí),但是由于paths字段的存在台谊,字段長(zhǎng)度總是有限的蓉媳,所以存在太深層的節(jié)點(diǎn)paths字段超出預(yù)設(shè)長(zhǎng)度的情況譬挚。當(dāng)然我們也可以設(shè)置成大文本字段,如mysql里面的text酪呻,即使這樣减宣,我們一般建議id是序列增長(zhǎng)的,如果像id是uuid這種玩荠,即使是大文本字段漆腌,paths的值也未免太恐怖了。
1.3左右值編碼
1.4閉包表
左右值編碼和閉包表的設(shè)計(jì)阶冈,筆者就不講了闷尿,因?yàn)楣P者并沒(méi)有對(duì)這兩種方案做過(guò)實(shí)際應(yīng)用,但不不代表這兩種方案不行女坑,存在即合理填具,他們也有自己的使用場(chǎng)景,大家可以自己搜索其相關(guān)方案的實(shí)現(xiàn)匆骗。
2劳景、菜單實(shí)體
在這里,我們先采用鄰接表的方式實(shí)現(xiàn)菜單模塊碉就,后面根據(jù)情況盟广,看是否需要再處理路徑枚舉的例子。
我們?cè)赾om.cangzhitao.springboot.study.security.entities包下新建Menu實(shí)體:
其中parent屬性瓮钥,這里使用了ManyToOne筋量,因?yàn)橐粋€(gè)節(jié)點(diǎn)可能有多個(gè)子節(jié)點(diǎn),多個(gè)子節(jié)點(diǎn)可能對(duì)應(yīng)一個(gè)父節(jié)點(diǎn)碉熄,所以對(duì)于parent來(lái)說(shuō)桨武,是ManyToOne,對(duì)于children來(lái)說(shuō)具被,是OneToMany玻募,兩者維護(hù)方式不一樣,ManyToOne是采用外鍵的方式一姿,OneToMany默認(rèn)是采用中間表的方式七咧,即使用中間表,一個(gè)字段記錄當(dāng)前節(jié)點(diǎn)id叮叹,另一個(gè)字段記錄當(dāng)前節(jié)點(diǎn)的所有直接子節(jié)點(diǎn)id艾栋。因?yàn)槲覀儧Q定采用鄰接表的方式,所以不會(huì)維護(hù)節(jié)點(diǎn)的子節(jié)點(diǎn)蛉顽。為了避免轉(zhuǎn)json串的時(shí)候出現(xiàn)死循環(huán)蝗砾,將parent屬性設(shè)置serialize=false,即不需要序列化,如果有需要悼粮,我們可以序列化一個(gè)parentid出來(lái)闲勺,沒(méi)必要整個(gè)對(duì)象。為了方便前端展示樹(shù)形結(jié)構(gòu)扣猫,又需要children屬性菜循,這里我們將屬性設(shè)置成Transient,表示不需要將它進(jìn)行持久化處理申尤。
我們?cè)偬砑右粋€(gè)輔助方法癌幕,方便添加子節(jié)點(diǎn)
3、Repository
Repository和之前的一樣
4昧穿、Controller
我們將PermController復(fù)制一份勺远,改成MenuController,先去掉代碼里面的權(quán)限驗(yàn)證时鸵,將對(duì)應(yīng)的Perm都改成Menu胶逢。我們的樹(shù)形結(jié)構(gòu)最終應(yīng)該將是這樣的效果
所以查詢(xún)頁(yè)面和普通對(duì)象的表格不一樣,是一棵樹(shù)寥枝,不需要分頁(yè)查詢(xún)什么的宪塔。樹(shù)的話(huà),一般有立即加載和延遲加載兩種囊拜,立即加載就是一次請(qǐng)求返回整個(gè)樹(shù)結(jié)構(gòu),一般適用于節(jié)點(diǎn)少的情況比搭,延遲加載則是最開(kāi)始只加載根節(jié)點(diǎn)冠跷,點(diǎn)擊一個(gè)節(jié)點(diǎn),再加載其直接子節(jié)點(diǎn)身诺,一般是處理節(jié)點(diǎn)數(shù)很多的情況蜜托。因?yàn)槲覀兊牟藛尾粫?huì)很多,所以我們這里使用立即加載的模式霉赡,需要后臺(tái)有個(gè)服務(wù)一次性將樹(shù)結(jié)構(gòu)返回橄务。
在getTree方法里面,我們先將所有的菜單查出來(lái)穴亏,然后將其循環(huán)放入了一個(gè)map里面蜂挪,然后再次遍歷每個(gè)節(jié)點(diǎn),如果該節(jié)點(diǎn)沒(méi)有父嗓化,或者父已經(jīng)被刪除(這種應(yīng)該屬于意外情況棠涮,父節(jié)點(diǎn)刪除了,子節(jié)點(diǎn)要么跟著一起刪除刺覆,要么將其父節(jié)點(diǎn)id進(jìn)行修改)严肪,就將其放入根節(jié)點(diǎn),其余的將放置對(duì)應(yīng)的父節(jié)點(diǎn)下面。這里做了兩次循環(huán)驳糯,兩次循環(huán)的目的是避免在第二次循環(huán)時(shí)篇梭,map中還未有相應(yīng)的父節(jié)點(diǎn)數(shù)據(jù),特定情況下酝枢,如果能確保循環(huán)是有序的恬偷,且先第一層根節(jié)點(diǎn),再第二層節(jié)點(diǎn)隧枫,第三層節(jié)點(diǎn)這樣喉磁,可以只做一次循環(huán)。如果需要排序官脓,可以在addChild方法里面做處理协怒。
5、樹(shù)形結(jié)構(gòu)展示頁(yè)面
我們從別的地方復(fù)制一個(gè)列表頁(yè)面卑笨,進(jìn)行對(duì)應(yīng)修改孕暇,查詢(xún)按鈕我們改成刷新按鈕,表格組件換成tree組件赤兴。
我們暫時(shí)先直接在數(shù)據(jù)庫(kù)增加幾條記錄妖滔,測(cè)試我們的頁(yè)面:
如果一切正常后,你將可以看到如下界面:
如果頁(yè)面有問(wèn)題桶良,請(qǐng)回顧上面的代碼座舍,看哪里有遺漏。
6陨帆、新增頁(yè)面
樹(shù)形結(jié)構(gòu)的新增和之前的新增還有點(diǎn)不同曲秉,樹(shù)形結(jié)構(gòu)的新增,我們需要選擇一個(gè)父菜單疲牵,當(dāng)然父菜單可以為空承二。我們先還是復(fù)制一個(gè)新增頁(yè)面,進(jìn)行修改纲爸。
我們?cè)O(shè)計(jì)使用http://localhost:8080/security/menu/add?parent=5?來(lái)進(jìn)行參數(shù)傳遞亥鸠,因?yàn)槲覀儾](méi)有啟用vue的路由,這里我們添加一個(gè)方法识啦,用來(lái)獲取url中的參數(shù)
我們先測(cè)試下负蚊,是否能正常取到參數(shù)。
測(cè)試成功后袁滥,我們將給查詢(xún)parent復(fù)制給表單
父我們可以設(shè)置成readonly盖桥,也可以做成可編輯的,因?yàn)橛脩?hù)可能在列表界面選錯(cuò)了父题翻,在新增界面我們可以讓用戶(hù)再次重新選擇父揩徊,但這樣頁(yè)面更加復(fù)雜腰鬼,我們需要再次彈窗,這里我們先按簡(jiǎn)單的來(lái)塑荒,做成只讀的熄赡。
新增頁(yè)面就做好了,請(qǐng)讀者自行測(cè)試下新增齿税。
接下來(lái)彼硫,我們把列表頁(yè)面和新增串起來(lái)。
修改列表頁(yè)面凌箕,暫時(shí)把三個(gè)按鈕都放出來(lái)
新增的時(shí)候拧篮,我們需要判斷當(dāng)前樹(shù)是否有選中一個(gè)節(jié)點(diǎn),選中的節(jié)點(diǎn)我們要獲取他的id牵舱,作為新增頁(yè)面的參數(shù)串绩。
這樣整個(gè)流程就串起來(lái)了。
7芜壁、編輯頁(yè)面
復(fù)制一份編輯頁(yè)面
測(cè)試發(fā)現(xiàn)頁(yè)面報(bào)錯(cuò)
這是由于我們把parent屬性設(shè)置了不序列化礁凡,導(dǎo)致get查詢(xún)的時(shí)候,返回到前端沒(méi)有了parent屬性慧妄。這里有很多個(gè)方案顷牌,我們?nèi)∠鹥arent的不序列化設(shè)置,然后修改之前的getTree方法塞淹,手動(dòng)將parent屬性都設(shè)置成null窟蓝。還一個(gè)是我們將parent序列化成parentid,然后前端根據(jù)parentid再進(jìn)行一次查詢(xún)饱普。如果parent不允許編輯疗锐,我們也可以不傳parent,或者只傳個(gè)parent.name用于顯示费彼,具體到不同的業(yè)務(wù)場(chǎng)景,大家可以自由選擇口芍,這里我們還是采取第一種方案吧箍铲。
再次測(cè)試,頁(yè)面正常了鬓椭,但編輯不生效颠猴,原來(lái)我們后臺(tái)的編輯方法的賦值操作還沒(méi)完成
測(cè)試ok后,我們將列表頁(yè)面和編輯頁(yè)面串起來(lái)小染。之前表格編輯翘瓮,我們的編輯刪除按鈕,都跟數(shù)據(jù)是在一行的裤翩,在tree組件里面资盅,我們也可以這么做,如下圖所示:
這里為了簡(jiǎn)單處理,我們將編輯和刪除按鈕呵扛,放到新增按鈕一起每庆。
8、刪除功能
上面說(shuō)了今穿,我們刪除節(jié)點(diǎn)的時(shí)候缤灵,要一并刪除子節(jié)點(diǎn),我們修改后臺(tái)的刪除方法蓝晒,做一個(gè)遞歸刪除
可以看到腮出,之類(lèi)遞歸刪除,確實(shí)會(huì)比較慢芝薇,會(huì)執(zhí)行多次sql胚嘲,這是效率最低的一種方法,一般來(lái)說(shuō)剩燥,可以?xún)?yōu)化成只執(zhí)行一個(gè)查詢(xún)sql慢逾,一個(gè)刪除sql的。
9灭红、權(quán)限
權(quán)限請(qǐng)大家自行完善
10侣滩、總結(jié)
這節(jié)介紹了樹(shù)形結(jié)構(gòu)常見(jiàn)的實(shí)現(xiàn)方案,因?yàn)槠邢薇淝埽瑢?xiě)了一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)君珠,只適用于數(shù)據(jù)量少的情形,大家可以自己嘗試下路徑枚舉的實(shí)現(xiàn)娇斑。
代碼:
https://github.com/www15119258/springboot-study/tree/branch25