在上一篇博客中,我提到了后台菜单的问题。其实我不想写,因为比较久了,都差不多忘了,只记得当时理解得很痛苦。
下面这个菜单是一个多层级菜单的,在 计算机中心 菜单下,有6个子菜单,在子菜单 微信管理 下面又有3个子菜单,子菜单 企业号管理 又有2个子菜单,层次可以无限地增加,而且层级也是不固定的。
那像这样的菜单结构在数据库应该怎么存储呢。
树!没错,就是用树型数据结构,在之前的博客中,我也有写过对树型数据结构相关的想法(树型数组库结构设计),主要用 parent_id 字段记录父节点 id 值形成树成关系,为了方便查询不用使用递归,添加一个 parent_ids 辅助字段来记录节点的全部上级节点 id 。
这次我不想再用这种数据结构了,想尝试一下网上介绍的左右值的树型数据结构。
左右值意义
先看一下这张图
如果你一眼就看出每个数字的意义,那请受我一拜~~~
这些菜单上左右两边的黑色数字就分别是节点的左右值了
请沿着根菜单的左边开始数起(1),往计算机中心的左边(2),再设备管理左边(3),设备管理右边(4),微信管理左边(5),服务号左边(6)。。。大家是否发现了规律。
把每个节点左右都当成两个连接点,从根菜单左边开始,沿着整个树的节点外围连接,一直连回到根菜单的右边,所经过节点的顺序就是节点的左右值了。
(不知这样表达大家是否能理解,我能想到的最通俗的表达也是这样了,不管,我就当是理解了)
很好,大家都理解了。
那我上一下菜单的数据库表图
这里面还添加了一个 level 字段,这个大家都知道,这是节点的层级,根菜单的层级是0。
查询
让大家见识一下这种数据结构的魅力,你会发现查询变得如此的简单,曾经复杂的子孙节点查询,现在变得如果简单。
查询某一节点下面的全部子孙节点
Transact-SQL
//例如想查询第一张图上的计算机中心这个菜单底下全部的子菜单
//计算机中心的左值为2 右值为11 level 为 1
//查询节点下全部子孙菜单不包括自身菜单
SELECT * FROM menu WHERE l > 2 AND r < 11
//查询节点下全部子孙菜单包括自身菜单
SELECT * FROM menu WHERE l >= 2 AND r <= 11
//如果你只想查询子节点,孙节点不想查询到,那么在 where 条件 添加 level = 2
1
2
3
4
5
6
7
8
9
10
//例如想查询第一张图上的计算机中心这个菜单底下全部的子菜单
//计算机中心的左值为2右值为11level为1
//查询节点下全部子孙菜单不包括自身菜单
SELECT*FROMmenuWHEREl>2ANDr<11
//查询节点下全部子孙菜单包括自身菜单
SELECT*FROMmenuWHEREl>=2ANDr<=11
//如果你只想查询子节点,孙节点不想查询到,那么在where条件添加level=2
计算某一节点下有多少个子孙节点
例如想查询第一张图上的计算机中心这个菜单底下有多少个子菜单
计算机中心的左值为2 右值为11。
子菜单数量(不包括自身节点)
( 右值 – 左值 – 1 ) / 2 => ( 11 – 2 – 1 ) / 2 = 4
子菜单数量(包括自身节点)
( 右值 – 左值 + 1 ) / 2 => ( 11 – 2 + 1) / 2 = 5
节点排序
当你在 sql 查询时排序条件加上 order by l 的话,你会发现,返回的节点是按节点全部展开后由上往下的顺序,这个非常有用,在下面的 菜单跟树表格中非常关键。
查询某一节点的全部父祖节点
Transact-SQL
例如想查询第一张图上的 企业号 菜单的有哪些父级菜单
企业号 的左值为8 右值为9 level 为 1。
SELECT * FROM menu WHERE l < 8 AND r > 9 order by l
//返回菜单并按由上往下排序,分别为根菜单,计算机中心,微信管理
//如果想由下往上就把排序条件改成 order by r
//如果你只是想查询某一节点到某父节点经过的路径的话
//例如想查询 企业号 到 计算机中心 经过的菜单
SELECT * FROM menu WHERE l < 8 AND l > 2 AND r > 9 AND r < 11 order by l
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
例如想查询第一张图上的企业号菜单的有哪些父级菜单
企业号的左值为8右值为9level为1。
SELECT*FROMmenuWHEREl<8ANDr>9orderbyl
//返回菜单并按由上往下排序,分别为根菜单,计算机中心,微信管理
//如果想由下往上就把排序条件改成orderbyr
//如果你只是想查询某一节点到某父节点经过的路径的话
//例如想查询企业号到计算机中心经过的菜单
SELECT*FROMmenuWHEREl<8ANDl>2ANDr>9ANDr<11orderbyl
判断
判断某一节点是否是某一节点的子节点
假如我想查询企业号menu1(左:8,右:9)是否是设备管理menu2(左:3,右:4)的子菜单。
判断式: menu1.l > menu2.l AND menu1.r < menu2.r
8 > 3 AND 9 <4 = false
说明企业号不是设备管理的子菜单
判断某一节点是否是某一节点的父节点
假如我想查询计算机中心menu1(左:2,右:11)是否是服务号menu2(左:6,右:7)的父菜单。
判断公式: menu1.l < menu2.l AND menu1.r > menu2.r
2 < 6 AND 11 > 7 = true
说明计算机中心是服务号的父菜单
判断某一节点是否有子菜单
将节点的右值减去左值,如果大于1则说明有。
相信以上的查询及判断已经可以满足对数据的日常使用了,那下面就来一个实际例子,就是我上个博客说到的 AdminLTE 后台菜单。
根据 AdminLTE 模板的 html 结构,以我第一个图为例,那生成的 html 代码如下所示(菜单不包括根菜单)
XHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
计算机中心
设备管理
微信管理
服务号
企业号
行政中心
车辆管理
人力资源
组织结构
员工档案
一个菜单就是一个 li ,如果菜单有子菜单的话,就得加上 treeview 的 css 样式,然后在里面嵌套一个 treeview-menu 样式的 ul ,再写上菜单 li ,以此类推,然后哪个菜单是当前打开的就在其 li 上及全部上级菜单 li 加上 active 样式。
我需要拼结的就是
基于thinkphp框架代码
PHP
public function getMenu(){
$admin_user=session("admin_user");
//如果当前用户不是超级管理员
if(-1!=$admin_user["id"]){
$role_id=$admin_user["role_id"];
$arr_menu_id=M("roleMenu")->where(array("role_id"=>$role_id))->getField("menu_id",true);
if(!$arr_menu_id){
return;
}
//到数据库查询权限对应菜单
$arr_menu=M()->table("menu m1,menu m2")->distinct(true)->field("m1.*")->where(array(
"m2.id"=>array("IN",$arr_menu_id),
"m1.l"=>array("EXP","<=m2.l"),
"m1.r"=>array("EXP",">=m2.r"),
"m2.r-m2.l"=>array("EXP","=1"),
"m1.level"=>array("gt","0")
))->order("m1.l")->select();
}else{
//超级管理员直接获取全部菜单
$arr_menu=D("Menu")->where(array("level"=>array("gt","0")))->order("l")->select();
}
//上面是根据当前登录用户获取对应的菜单,大家可以不用理会,直接当成是超级管理员获得全部菜单
//在数组中查找当前的active的菜单
$current_menu;
$controller=CONTROLLER_NAME;//获取当前的控制器
if($controller!=="Index"){
foreach ($arr_menu as $m) {
if(CONTROLLER_NAME===$m[