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 不適合特別複雜的邏輯
目前我還沒有遇到可能會超越這種資料結構的需求,但是難免客戶會從一些刁鑽的角度提出另類的需求,而這種資料結構其實從根本上來說是十分精巧脆弱的,一旦出現了變動,很可能會增加實現邏輯的複雜度,甚至於推倒重來。
因此需要小心的權衡這種資料所帶來的利弊。
最後
這種新型的資料結構,是經過錘鍊與驗證的,確實從實際上幫助你解決一些業務痛點。
但它同樣也是一把雙刃劍,能夠完整的駕馭它,需要較強的邏輯思維與程式碼設計能力。
否則最後的程式碼結構,會看起來到處是為了實現這種邏輯打上的補丁,因此不斷的重構、精簡程式碼、完善的迴歸測試,也是十分必要的。