在平常的业务系统开发中,我们经常需要设计数据层次关系,如在经典的user-role-permission权限设计中, 需要对权限表的数据设计成一种层次依赖关系,如最顶层的为系统管理,系统管理的下一层为角色 管理,角色管理的下一层又为角色的CRUD操作, 那么这种表就可以抽象成为数据结构里面的B树. 如下表 :
CREATE TABLE "U_PERMISSION"
( "ID" NUMBER(20,0),
"URL" VARCHAR2(256 BYTE),
"NAME" VARCHAR2(50 BYTE),
"PARENT" NUMBER(20,0)
)
接着我们插入测试数据 :
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (1,'*','系统管理',0);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (2,'*','权限管理',1);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (20,'/role/allocation','角色分配',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (4,'/permission/index','权限列表',2);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (6,'/permission/addPermission.shtml','权限添加',2);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (7,'/permission/deletePermissionById','权限删除',2);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (8,'/member/list.shtml','用户列表',22);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (9,'/member/online.shtml','在线用户',22);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (10,'/member/changeSessionStatus','用户Session踢出',22);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (11,'/member/forbidUserById','用户激活or禁止',22);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (12,'/member/deleteUserById','用户删除',22);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (13,'/permission/addPermission2Role','权限分配',2);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (14,'/role/clearRoleByUserIds','用户角色分配清空',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (15,'/role/addRole2User','角色分配保存',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (16,'/role/deleteRoleById.shtml','角色列表删除',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (17,'/role/addRole','角色列表添加',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (18,'role/index','角色列表',23);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (19,'/permission/allocation','权限分配2',2);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (22,'*','用户管理',1);
Insert into U_PERMISSION (ID,URL,NAME,PARENT) values (23,'*','角色管理',1);
既然都已经创建树形表了那么肯定要对树进行操作。就像我们在大学的数据结构一书中所做的那样对树进行递归遍历(前序,中序,后序. 忘记了的面壁....) 在oralce中通过
start with....connect by...prior语法,依托该语法我们就可以对上面树形表进行递归遍历。
示例:
给出一个节点的值,求出的他父节点和祖宗节点:
start with子句: 递归的条件,需要注意的是如果with后面的值是子节点那么求出的就是他的父节点和祖宗节点,如果是父节点那么求出的就是他的子节点和子孙节点,
如果不懂可以把上面start with 后面的条件改成 p.parent=0,就可以理解了。
connect by子句:连接条件。 关键词prior,prior跟它右边的父节点放在一起(prior p.parent)表示往父节点方向遍历, 反之,如果 prior跟子节点放在一起(prior p.id)表示往
叶子方向遍历。 这里需要注意的 =p.id 放在prior关键词的前面或者后面都没什么关系,也就是上面可以这样写 p.id= prior p.paren。重要的是prior旁边放的
是什么。
level伪列: 递归的层次表示, 用来进行输出缩进。可以看到递归层次,看起来很直观。 需要注意的是Level 也可以放在Group by后面,也可以放在select 后面.
下面我来讲一下递归语法的一些用法和一些需要注意的地方:
有趣的是我们可以不使用start with 子句,它不像程序语言一样不写就会造成死循环(插一句嘴,曾经有一人和我聊天提到SQL不是编程语言,因为他不满足图灵完全。因为他并不能造成死循环,其实通过递归语句就可以,这个坑我会在后面演示,在本文中并不讨论SQL是不是编程语言)。它只会把会整个表的数据遍历一遍,每一个数据做一次
根节点,然后遍历树中的其他节点。
他等价于如下SQL:
select p.ID,p.NAME,p.PARENT, level from u_permission p start with p.PARENT in( '祖宗节点','父节点','子节点','孙子节点' ) connect by p.id= prior p.parent;
2. start with 和connect by prior的位置可以互换:
select p.ID,p.NAME,p.PARENT, level from u_permission p start with p.PARENT=1 connect by p.id= prior p.parent;
select p.ID,p.NAME,p.PARENT, level from u_permission p connect by p.id= prior p.parent start with p.PARENT=1;
上面二条的SQL意思是一样的。
3.我们可以把start with看成是一个where,所以这也意味着我们可以在我们可以在除了最后部分以外的地方加where条件, 如:
select p.ID,p.NAME,p.PARENT, level from u_permission p where p.id=xxx start with p.PARENT=1 connect by p.id= prior p.parent;
但是这里隐藏着一个坑,
where条件的作用域不是在递归查询中的,而是在递归查询完后的。 所以如果理解不当他可以造成死循环(虽然会检测出来报ORA错误)也就是a的父节点是b, b的父节点是a。
看一个实用的列子:
如果有一个需求如根据user表的id查出他的权限树,这个时候我们就需要start with后面加子查询。
我们可以看到根据子查询返回的根节点查出所有子节点的父亲节点和祖宗节点。
下面是一些跟递归语法相关的函数(来自Oracle SQL高级编程):
SYS_CINNECT_BY_PATH函数:
这个函数是用来返回组成层级的直到当前行的值, 我一图胜千言把。
简单来说就是用来做拼接的,至于想拼接什么看各位看官的心情咯。
CONNECT_BY_ISLEAF伪列:
这个伪列呢,说直白一点跟level的作用差不多,只不过他是用来在递归查询中显示叶子节点而非递归深度。 留给看官实验,不上传实验图片了。
CONNECT_BY_ISCYCLE伪列 和NOCYCLE:
这个函数用于检测树中是不是出现了环,传统的数据结构树是不会出现环的,只有在图中才会出现环。 而在数据库中我说过,他不是正儿八经
的B树,他可能会出现一些不合人伦的事情,比如就像上面所说的死循环, a是b的父亲,b又叫a儿子这样尴尬的事情,Oracle会直接报出ORA-1436的
错误,告诉你这样不太好。 所以我们可以用nocycle函数解决这个问题,也就是让b当儿子。使得SQL符合逻辑。
select connect_by_isleaf ,p.ID,p.NAME,p.PARENT, CONNECT_BY_ISCYCLE , level from u_permission p start with p.PARENT=1 connect by nocycle p.id= prior p.parent;
connect_by_isleaf 函数:
在递归中检测当前节点是否包含下级节点,也就是说是不是叶子节点,是返回0, 不是返回1, 在动态的目录中有用。
select connect_by_isleaf ,p.ID,p.NAME,p.PARENT, level from u_permission p start with p.PARENT is null connect by p.id= prior p.parent;