mysql樹形結構表設計(Path Enumerations,Closure Table)
效果
首先看下返回到前臺的效果:
下面是返回給前臺的json:
{
"code": 1,
"data": [
{
"children": [
{
"children": [
{
"id": 76,
"parent_role_id": 74,
"parentname": "管理員1號",
"role_name": "群員1號",
"role_type": "111"
},
{
"id": 77,
"parent_role_id": 74,
"parentname": "管理員1號",
"role_name": "群員2號",
"role_type": "2222"
}
],
"id": 74,
"parent_role_id": 73,
"parentname": "群主" ,
"role_name": "管理員1號",
"role_type": "111"
},
{
"children": [
{
"id": 78,
"parent_role_id": 75,
"parentname": "管理員2號",
"role_name": "群員3號",
"role_type": "333"
}
] ,
"id": 75,
"parent_role_id": 73,
"parentname": "群主",
"role_name": "管理員2號",
"role_type": "222"
}
],
"id": 73,
"parent_role_id": 0,
"role_name": "群主",
"role_type": "111"
}
],
"msg": "執行成功"
}
引言
一般比較普遍的就是四種方法:(具體見 SQL Anti-patterns這本書)
Adjacency List:每一條記錄存parent_id
Path Enumerations:每一條記錄存整個tree path經過的node列舉
Nested Sets:每一條記錄存 nleft 和 nright
Closure Table:維護一個表,所有的tree path作為記錄進行儲存。
各種方法的常用操作代價見下圖
正文
如上所見,我們選擇方案2和方案4來進行設計
方案2(Path Enumerations)
簡單說說,圖中的path存的是每個層級的關係,像第6條記錄的意思就是我的上級是5,5的上級是2,2的上級是1,其他記錄同理可得。
這種方法乍看很清晰很明瞭,但其實維護是比較難維護的,而且有可能會產生髒資料,畢竟你所維護的都是通過字串來維護
不做過多介紹,因為這個方案確實是最簡單的,如上所說,每種方案都適用不同業務。
方案4(Closure Table)
這個方案我也是參考上面的部落格,可以先看看他的表結構圖
下面是本文的表結構:
這是角色表,包含角色名稱,父角色ID
這是角色關係表,ancestor_id是父級和祖父級ID,level是當前角色相對ancestor_id而言的層次,rold_id則是角色ID
舉個例子,群主這個角色是屬於最頂層,那麼只插入一條關聯資料,表示自身,即 73,0,73
管理員1號是屬於群主下面的一個角色,那麼首先同樣需要先建立一個表示自身的資料,即74,0,74 ,然後建立管理員1號和群主關聯的資料,即73,1,74 這裡的1就是代表管理員1號對於群主而言,他是一級直屬關係,所以level為1
規律
每個子角色都需要和自身以及所有的上級角色關聯,假設有5個等級,A1-A2-A3-A4-A5,當建立A5的時候,先插入自身資料,然後迴圈所有上級進行關聯。
程式碼部分
public class RoleTree {
private Integer id;
private Integer ancestor_id;
private Integer level;
private Integer role_id;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getAncestor_id() {
return ancestor_id;
}
public void setAncestor_id(Integer ancestor_id) {
this.ancestor_id = ancestor_id;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
public Integer getRole_id() {
return role_id;
}
public void setRole_id(Integer role_id) {
this.role_id = role_id;
}
}
新增操作程式碼:
private void insertIntoRoleTreeTable(Role record) {
//插入自身資料
RoleTree roleTree = new RoleTree();
roleTree.setAncestor_id(record.getId());
roleTree.setLevel(0);
roleTree.setRole_id(record.getId());
roleTreeMapper.insert(roleTree);
if (record.getParent_role_id() == 0) {
//如果父級ID為0代表他是最根部的角色,則只插入自身資料
return;
}
//插入自身和父級關聯的資料
roleTree.setAncestor_id(record.getParent_role_id());
roleTree.setLevel(1);
roleTree.setRole_id(record.getId());
roleTreeMapper.insert(roleTree);
//查詢父級的所有上級
List<RoleTree> roleTreeList = roleTreeMapper.selectAllParentOfRole(record.getParent_role_id());
//迴圈父級並和新插入的資料關聯
for (RoleTree aRoleTreeList : roleTreeList) {
RoleTree r = new RoleTree();
r.setRole_id(record.getId());
r.setLevel(aRoleTreeList.getLevel() + 1);
r.setAncestor_id(aRoleTreeList.getAncestor_id());
roleTreeMapper.insert(r);
}
}
<!--查詢某個角色的所有上級-->
<select id="selectAllParentOfRole" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from b_role_tree_relation
where role_id = #{role_id,jdbcType=INTEGER}
and level != 0
</select>
<!--查詢某個角色的所有下級-->
<select id="selectAllChildOfRole" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from b_role_tree_relation
where ancestor_id = #{ancestor_id,jdbcType=INTEGER}
and level != 0
</select>
<!--刪除某個角色的所有關係-->
<select id="deleteTreeRelationOfRole" parameterType="java.lang.Integer">
delete from b_role_tree_relation where role_id in (
select role_id from (SELECT role_id FROM b_role_tree_relation where ancestor_id= #{role_id,jdbcType=INTEGER} ) a
)
</select>
更新操作:
由於子角色在更新的時候,可能會從一個層級很深的角色變成根部角色或者根部的直屬角色,如果使用更新不僅要刪除原來資料,還要判斷更新資料ID,因此我的做法是直接刪除原有的所有關係,再重新插入新的關係
總結
雖然方案4處理起來較為麻煩,但是確實適用於各種業務的方案,而且他不會產生髒資料,維護以及管理時也能一目瞭然.