1 前言

权限架构一直是企业中,中小型系统绕不开的功能,这篇文章中,涉及到的篇幅仅限于不是特别复杂的中小型系统。

以我遇到的为例,权限架构一般分为组织管理、角色管理、用户管理。

组织管理,掌管的是数据权限,一个组织的横向之间相互隔离,纵向之间确定能见度,

角色管理,掌管的是功能权限,不同的角色有不同的菜单,有不同的功能。

用户管理,是组织与角色的共同体现。

在这里,重点要说的是组织管理,因为在不同的系统中,角色本身代表的含义差别很大,角色可能有上下级管理,也可能没有,或许是能看到的菜单不同,或许是能看到的按钮不同,这些功能的他体现还要根据前端架构的不同进行设计,可以说没有什么准确的设计,能够涵盖角色功能代表的意义。

但是组织一般共性较多一些,组织与前端关系不是很深,一般只需要一个管理功能,用户能够挂靠到组织下即可,主要还是看后端如何处理不同组织关系下的数据查询。

2 组织架构数据结构一般形式

在一般系统中,大部分设计组织架构,都是使用一张表,利用 pid 作为关联本表 id 的外键,使用 pid 与 id 的层层递进的关联关系。

大概类似于下面的表格

idnamepid
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 纵向数据关系表

纵向数据关系表示如下表,可以看到,表示层级关系的高低,是通过增加层级的深度的来表示的。

idpid
0null
10
21

3.2 横向数据关系表

那么使用横向数据关系表示呢?如下表。在下面表中,表示层级关系是通过横向扩展数据的长度来表示的,层级关系的高低,取决于横向数据的长短。(order_seq 表示自己在当前层级下的顺序,为的是方便数据处理,tier 为数据的层级,是否需要取决于实际场景能否用到,例如当希望能够查询指定深度的数据时)

idauthorityorder_seqtier
00/00
10/1/11
20/1/0/02

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/10/10,在上面的 sql 中,若没有最后一位 / 的阻隔,查询结果就会出现数据异常。

3.3.3 数字的选择

在这种设计中,数字的选择的正确与否至关重要,它是整个数据结构的基础。

需要准守下面两个要点:

  1. 有相同上级的同级中不允许出现重复数字,有不同上级的同级可以出现重复数字,例如 0/0/1/0/
  2. 尽量不让数字只增大,而是能够填补之前的空缺,例如你有 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 分为三部分。

  1. 第一部分是为了找到缺失的数字,思路是利用 order_seqrow_num 的不相等记录。
  2. 第二部分是为了找到最大的数字。
  3. 最后合并第一部分与第二部分的结果,只取前一位,若此 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 不适合特别复杂的逻辑

目前我还没有遇到可能会超越这种数据结构的需求,但是难免客户会从一些刁钻的角度提出另类的需求,而这种数据结构其实从根本上来说是十分精巧脆弱的,一旦出现了变动,很可能会增加实现逻辑的复杂度,甚至于推倒重来。

因此需要小心的权衡这种数据所带来的利弊。

最后

这种新型的数据结构,是经过锤炼与验证的,确实从实际上帮助你解决一些业务痛点。

但它同样也是一把双刃剑,能够完整的驾驭它,需要较强的逻辑思维与代码设计能力。

否则最后的代码结构,会看起来到处是为了实现这种逻辑打上的补丁,因此不断的重构、精简代码、完善的回归测试,也是十分必要的。


zxdposter
3.9k 声望3.5k 粉丝