Spring Boot入門(mén)(第二十五章):菜單(樹(shù)形結(jié)構(gòu)的設(shè)計(jì))

這節(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末策添,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子毫缆,更是在濱河造成了極大的恐慌唯竹,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苦丁,死亡現(xiàn)場(chǎng)離奇詭異浸颓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)旺拉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)产上,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蛾狗,你說(shuō)我怎么就攤上這事晋涣。” “怎么了沉桌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵谢鹊,是天一觀(guān)的道長(zhǎng)算吩。 經(jīng)常有香客問(wèn)我,道長(zhǎng)撇贺,這世上最難降的妖魔是什么赌莺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮松嘶,結(jié)果婚禮上艘狭,老公的妹妹穿的比我還像新娘。我一直安慰自己翠订,他們只是感情好巢音,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著尽超,像睡著了一般官撼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上似谁,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天傲绣,我揣著相機(jī)與錄音,去河邊找鬼巩踏。 笑死秃诵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的塞琼。 我是一名探鬼主播菠净,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彪杉!你這毒婦竟也來(lái)了毅往?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤派近,失蹤者是張志新(化名)和其女友劉穎攀唯,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體渴丸,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡革答,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了曙强。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡途茫,死狀恐怖碟嘴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情囊卜,我是刑警寧澤娜扇,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布错沃,位于F島的核電站,受9級(jí)特大地震影響雀瓢,放射性物質(zhì)發(fā)生泄漏枢析。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一刃麸、第九天 我趴在偏房一處隱蔽的房頂上張望醒叁。 院中可真熱鬧残家,春花似錦迅栅、人聲如沸秘症。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)矩肩。三九已至铡原,卻和暖如春纱新,著一層夾襖步出監(jiān)牢的瞬間篮奄,已是汗流浹背捆愁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窟却,地道東北人昼丑。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像间校,于是被迫代替她去往敵國(guó)和親矾克。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354