目標(biāo)
在一顆樹中快速查找到子孫節(jié)點(diǎn)复局、祖先節(jié)點(diǎn)
查找節(jié)點(diǎn)A的所有子孫節(jié)點(diǎn)
樹的表結(jié)構(gòu)
CREATE TABLE `node` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT '' COMMENT '節(jié)點(diǎn)名稱',
`parent_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
node表的數(shù)據(jù)
id | name | parent_id |
---|---|---|
1 | A | 0 |
2 | B | 1 |
3 | C | 1 |
4 | D | 2 |
5 | E | 2 |
6 | F | 3 |
查找節(jié)點(diǎn)A的子孫節(jié)點(diǎn)马篮,可以用以下3個SQL完成
select … where parent_id = A ==> B, C
select … where parent_id in (B, C) ==> D,E,F
select … where parent_id in (D,E,F) ==> empty
可以看到隨著節(jié)點(diǎn)深度的增加,獲取子孫節(jié)點(diǎn)SQL條數(shù)會越來越多箱叁。
如何進(jìn)行優(yōu)化墅垮,來避免和SQL的多次交互?閉包表一次就能獲取節(jié)點(diǎn)的子孫部門或者祖先部門
什么是閉包表
一張記錄樹中所有節(jié)點(diǎn)的關(guān)系表耕漱。記錄節(jié)點(diǎn)之間距離的關(guān)系表算色,注意:這次討論的閉包關(guān)系表不包含自身的關(guān)系,例如 (ancestor,descendant,distance) => (A,A,0)
表結(jié)構(gòu)如下:
CREATE TABLE `node_relation` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`ancestor` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '祖先節(jié)點(diǎn)',
`descendant` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '后代節(jié)點(diǎn)',
`distance` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '相隔層級螟够,>=1',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_anc_desc` (`ancestor`,`descendant`),
KEY `idx_desc` (`descendant`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='節(jié)點(diǎn)關(guān)系表'
node_relation表A作為祖先節(jié)點(diǎn)的的關(guān)系數(shù)據(jù)(id 用實際節(jié)點(diǎn)描述表示)
ancestor | descendant | distance |
---|---|---|
A | B | 1 |
A | C | 1 |
A | D | 2 |
A | E | 2 |
A | F | 2 |
再來看查A部門的所有子部門怎么查,只需要一個SQL就搞定了
select descendant from ancestor = A;
閉包表副作用
由于閉包表新增了節(jié)點(diǎn)和節(jié)點(diǎn)之間的關(guān)系灾梦,所以在變更樹結(jié)構(gòu)的時候,會重構(gòu)這個關(guān)系妓笙,想想就覺得復(fù)雜若河。所以數(shù)據(jù)量少,請謹(jǐn)用閉包表寞宫。
如果實在是需要用到閉包表的萧福,那么請繼續(xù)往下看,本文會為你理清這些關(guān)系辈赋。
閉包表常見操作
閉包表-查詢
1).獲取指定A節(jié)點(diǎn)的子孫節(jié)點(diǎn)
select descendant from node_relation where ancestor = A;
2).獲取指定節(jié)點(diǎn)F的祖先節(jié)點(diǎn)
select ancestor from node_relation where descendant = F;
閉包表-增鲫忍、刪、move
1).新增:在F節(jié)點(diǎn)下新增節(jié)點(diǎn)H(見圖3)
//新增節(jié)點(diǎn)H
insert into node (`name`,`parent_id`) values ("H",F_id);
//記錄F钥屈、H的閉包關(guān)系
insert into node_relation (ancestor,descendant,distance) values(F,H,1);
;//獲取F和祖先閉包關(guān)系
select ancestor,descendant,distance from node_relation where descendant = F
H和F的祖先閉包關(guān)系很明顯可以發(fā)現(xiàn)
F的祖先 悟民,F(xiàn) ,distance = F的祖先篷就,H射亏,distance+1
java 偽代碼:
Long HID=999L;
Long FID = 888L;
List<NodeRelationDO> nodeSelectResult = nodeRelationDao.queryAncestor(FID);
List<NodeRelationDO> nodeRelations = new ArrayList<>();
for (NodeRelationDO item : nodeSelectResult) {
NodeRelationDO nodeRelationDO = new NodeRelationDO();
nodeRelationDO.setAncestor(item.getAncestor);
nodeRelationDO.setDescendant(HID);
nodeRelationDO.setDistance(item.getDistance + 1);
nodeRelations.add(nodeRelationDO);
}
nodeRelationDao.batchUpsert(nodeRelations);
2).刪除:刪除C節(jié)點(diǎn)(遞歸刪除)
刪除比較簡單,主要分以下2個步驟
- 刪除節(jié)點(diǎn)
- 刪除節(jié)點(diǎn)閉包表相關(guān)數(shù)據(jù)
紅色的是需要刪除的關(guān)系邊(見圖4)竭业,其實很容易就找出規(guī)律智润,需要刪除和刪除節(jié)點(diǎn)有關(guān)的所有關(guān)系邊
//刪除節(jié)點(diǎn)閉包表相關(guān)數(shù)據(jù)
delete from node_relation where ancestor in (需要刪除集合)
or descendant in (需要刪除的集合);
3).變更父節(jié)點(diǎn): 將B節(jié)點(diǎn)移到C節(jié)點(diǎn)上(見圖5)
- 從節(jié)點(diǎn)考慮只需要把B節(jié)點(diǎn)parent_id 直接更新成C就完成了
- 閉包關(guān)系表變化會復(fù)雜一些
接下去介紹下,move過程閉包關(guān)系表的變化
1).待刪除的邊:
select * from node_relation where ancestor in (B的所有祖先)
and descendant in (B樹的所有節(jié)點(diǎn),包含B);
刪除后會得到2顆分裂的樹,見圖6 ,紅色邊就是等待刪除的邊
2).重建關(guān)系
新增關(guān)系:(C及C的所有祖先節(jié)點(diǎn)) x (B樹的所有節(jié)點(diǎn))未辆,這兩個節(jié)點(diǎn)集合的笛卡爾積就是新增的邊(見圖7)做鹰。