1 前言
权限架构一直是企业中,中小型系统绕不开的功能,这篇文章中,涉及到的篇幅仅限于不是特别复杂的中小型系统。
以我遇到的为例,权限架构一般分为组织管理、角色管理、用户管理。
组织管理,掌管的是数据权限,一个组织的横向之间相互隔离,纵向之间确定能见度,
角色管理,掌管的是功能权限,不同的角色有不同的菜单,有不同的功能。
用户管理,是组织与角色的共同体现。
在这里,重点要说的是组织管理,因为在不同的系统中,角色本身代表的含义差别很大,角色可能有上下级管理,也可能没有,或许是能看到的菜单不同,或许是能看到的按钮不同,这些功能的他体现还要根据前端架构的不同进行设计,可以说没有什么准确的设计,能够涵盖角色功能代表的意义。
但是组织一般共性较多一些,组织与前端关系不是很深,一般只需要一个管理功能,用户能够挂靠到组织下即可,主要还是看后端如何处理不同组织关系下的数据查询。
2 组织架构数据结构一般形式
在一般系统中,大部分设计组织架构,都是使用一张表,利用 pid 作为关联本表 id 的外键,使用 pid 与 id 的层层递进的关联关系。
大概类似于下面的表格
id | name | pid |
---|---|---|
0 | 顶层组织 | null |
1 | 二层组织 | 0 |
2 | 三层组织 | 1 |
这样我们通过 pid 为 null
来确定这是顶层机构,通过 pid 为 1 的记录找到二层组织的子记录,如果想找到三层组织,还需要运用复杂的 sql,来重新组织数据关系,来方便找到一连串的组织关系。
例如这段在 oracle 中的 sql,这样的 sql 可以用来找到顶层组织所有的关联关系。
SELECT * FROM table_organ
START WITH id = '0'
CONNECT BY NOCYCLE PRIOR ID = pid
这样做问题不大,如果说只局限于组织关系表这一张表的查询,那么我们的问题永远也不用上升层次。
但是针对于关联表的查询,这样的方式就显得有些笨重了。如果你要查询,顶层机构及其隶属机构在某一张表的所属数据,就需要用到下面的 sql:
select * from some_table where organId in (SELECT id FROM table_organ
START WITH id = '1'
CONNECT BY NOCYCLE PRIOR ID = pid)
这样的实现没有任何问题,目前的数据库肯定已经考虑到了在条件不变的情况下,子查询只会查询一次,也就是说,这样的查询,副作用只是会多带来一次查询。
但是在经历了多次这样的功能设计之后,旧的情况已经渐渐不能满足于我了。
3 组织架构数据结构的新形式
表现组织层级的不一定要使用纵向的数据关系,也可以使用横向的数据关系。
什么是纵向、横向?下面来解释一下。
3.1 纵向数据关系表
纵向数据关系表示如下表,可以看到,表示层级关系的高低,是通过增加层级的深度的来表示的。
id | pid |
---|---|
0 | null |
1 | 0 |
2 | 1 |
3.2 横向数据关系表
那么使用横向数据关系表示呢?如下表。在下面表中,表示层级关系是通过横向扩展数据的长度来表示的,层级关系的高低,取决于横向数据的长短。(order_seq 表示自己在当前层级下的顺序,为的是方便数据处理,tier 为数据的层级,是否需要取决于实际场景能否用到,例如当希望能够查询指定深度的数据时)
id | authority | order_seq | tier |
---|---|---|---|
0 | 0/ | 0 | 0 |
1 | 0/1/ | 1 | 1 |
2 | 0/1/0/ | 0 | 2 |
3.3 数据结构的设计要点
3.3.1 查询方式
在利用了横向设计的数据之后,查询就变得异乎寻常的简单。
当我希望查询顶层组织的所有关联关系时:
SELECT * FROM table_organ where authority like '0/%'
当我希望查询所属顶层组织的数据时:
select * from some_table where authority like '0/%'
当我希望查询通过中间一层组织,找到所有上下级组织的数据时:
select * from some_table where authority like '0/1/%' or instr('0/1/', authority) = 1
3.3.2 分隔符
在上面的 3.2 中的横向数据关系表中,你能够看到我使用的是分割是 /
,并且数据也要以 /
分隔符作为结尾。
这是为了防止在下一层数据过多时,导致的数据判断异常,例如 0/1
和 0/10
,在上面的 sql 中,若没有最后一位 /
的阻隔,查询结果就会出现数据异常。
3.3.3 数字的选择
在这种设计中,数字的选择的正确与否至关重要,它是整个数据结构的基础。
需要准守下面两个要点:
- 有相同上级的同级中不允许出现重复数字,有不同上级的同级可以出现重复数字,例如
0/0/
和1/0/
- 尽量不让数字只增大,而是能够填补之前的空缺,例如你有
0/
、1/
、3/
这三个顶层组织,当1/
被删除之后,再次添加需要能够填补1
的空缺
由此,设计出一个 sql,来帮助我们实现这个需求,sql 为 mysql 的版本。
select *
from (select *
from (select c.rownum
from (SELECT @rownum := @rownum + 1 as rownum, a.order_seq
from (select *
from cem_organ
where authority regexp '^[0-9]+/$') a,
(SELECT @rownum := -1) b
ORDER BY a.order_seq) c
where c.rownum != c.order_seq
limit 1) as e
UNION
select *
from (
select max(d.order_seq) as num
from cem_organ d
where d.authority regexp '^[0-9]+/$') as f
where f.num is not null) as g
limit 1
这个 sql 分为三部分。
- 第一部分是为了找到缺失的数字,思路是利用
order_seq
与row_num
的不相等记录。 - 第二部分是为了找到最大的数字。
- 最后合并第一部分与第二部分的结果,只取前一位,若此 sql 没有返回任何记录,那么程序取 0。
使用这个 sql 的同时,也要注意到利用 authority 通过正则,来限制你的上级,若是顶层组织,值为 ^[0-9]+/$
,若是非顶层组织,值为 ^0/[0-9]+/$
,以此类推 ^0/1/[0-9]+/$
。(也可以通过 like 的形式做到同样的效果)
在这里还需要注意一个关键的问题,那就是并发产生的问题,如果同一时间执行此条 sql,会造成取值相同的情况,因此需要在执行 sql 的地方加上分布式锁,来确保并发情况下能够得到唯一的值。锁的值使用父级 authority 来提高一些性能。
3.4 优点
3.4.1 查询逻辑简单容易移植
这个优点是显而易见的,当你想找到组织关系中的所有的下级关系,查询逻辑只需要用一个 like
,并且 like
基本上所有的数据库语法都是一样的,因为简单,所以容易移植。
regex
查询同样也是如此。
3.4.2 查询速度快
在拥有百万数据级别时,特别是查询的条件为拥有许多隶属下级的顶层组织时,优点尤为明显。
另外,在有两层或者两层以上的上下级关系之间查询,有着无可比拟的优势。
例如,当组织下分管着部门,部门下又分管着打印机,当你把相同的思路应用于部门表以及打印机表时,你完全可以跳过组织、部门,直接查询。
select * from table_printer where authority like '0/%'
在这里,你可以联想一下,如果使用 pid
关联的方式,该如何查询,特别是在一个大的组织关系中,当部门的数据量过千时,怎么破解数据库对 in
数量的限制,也是需要考虑的地方。
3.4.1 数据更容易理解
相比于之前的 pid
连接方式,authority
更加能够直观的体现数据之间的关系。
3.5 缺点
3.5.1 实现逻辑复杂
由前面的文章可以看出,为了实现这种基于数据结构的上下级关系,我们需要满足诸多条件,需要在各个地方小心翼翼的维护 authority
代表上下级关系的字符串,一旦出错,带来的影响将是巨大的。
3.5.2 不适合特别复杂的逻辑
目前我还没有遇到可能会超越这种数据结构的需求,但是难免客户会从一些刁钻的角度提出另类的需求,而这种数据结构其实从根本上来说是十分精巧脆弱的,一旦出现了变动,很可能会增加实现逻辑的复杂度,甚至于推倒重来。
因此需要小心的权衡这种数据所带来的利弊。
最后
这种新型的数据结构,是经过锤炼与验证的,确实从实际上帮助你解决一些业务痛点。
但它同样也是一把双刃剑,能够完整的驾驭它,需要较强的逻辑思维与代码设计能力。
否则最后的代码结构,会看起来到处是为了实现这种逻辑打上的补丁,因此不断的重构、精简代码、完善的回归测试,也是十分必要的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。