1.前言
幾乎在每一個系統(tǒng)的開發(fā)過程中,都會遇到一些樹狀結(jié)構(gòu)的開發(fā)需求,例如:組織機(jī)構(gòu)樹,部門樹,菜單樹等。只要是需要開發(fā)這種樹狀結(jié)構(gòu)的需求,我們都可以使用組合模式來完成。
本篇將結(jié)合組合模式與Mysql
實現(xiàn)一個部門樹,完成其增刪改和樹形結(jié)構(gòu)的組裝。
2.組合模式
組合模式是一種結(jié)構(gòu)型設(shè)計模式,它允許我們將對象組合成樹形結(jié)構(gòu)來表現(xiàn)部分-整體的層次結(jié)構(gòu)。以部門樹為例,我們可以將上級部門與下級部門組合起來,形成一個單邊樹,用代碼來描述的話,就是這個樣子的:
public class DeptNode {
private List<DeptNode> children = new ArrayList<>();
}
提供一個部門節(jié)點(diǎn)類,里面會有一個集合,用于保存當(dāng)前部門的下級部門,同理在children
這個集合中的部門節(jié)點(diǎn),也可能會有它的下級部門節(jié)點(diǎn)。
當(dāng)然,這不是實現(xiàn)組合模式的唯一方式,還有其他復(fù)雜一點(diǎn)方式,會區(qū)分不同的節(jié)點(diǎn)類型,是根節(jié)點(diǎn)、分支節(jié)點(diǎn)、還是葉子節(jié)點(diǎn)等。這里之所以做這種簡單的設(shè)計,是因為我們的樹狀結(jié)構(gòu)的數(shù)據(jù)一般都會交給前端去做渲染,在很多前端的組件庫中,就是用這種簡單的方式來組織樹的,例如在Element-UI
中的樹狀結(jié)構(gòu):
3.實現(xiàn)方式
3.1.數(shù)據(jù)結(jié)構(gòu)設(shè)計
先看數(shù)據(jù)庫的設(shè)計,數(shù)據(jù)庫必要的字段比較簡單,直接看一下建表的sql:
create table dept
(
id bigint auto_increment comment '部門id'
primary key,
parent_id bigint null comment '上級部門id',
name varchar(200) null comment '部門名稱',
tree_path varchar(255) null comment '樹路徑'
)
id
與parent_id
很好理解,主要是用來維護(hù)部門的上下級關(guān)系,name
不解釋,tree_path
這個字段其實不是必須要的,沒有它也可以實現(xiàn)部門樹,但是加上這個path
之后,可以比較方便的查詢子樹。
PO
對象與數(shù)據(jù)庫字段保持一致,這里就不過多贅述,代碼中需要返回給前端的樹對象要修改一下字段名,name
->label
:
@Getter
@Setter
public class DeptNode {
private List<DeptNode> children = new ArrayList<>();
private Long id;
private Long parentId;
private String label;
private String treePath;
}
3.2.數(shù)據(jù)新增
由于是自增主鍵,數(shù)據(jù)的新增需要再保存之后獲取到主鍵id,再更新treePath
。
這里為了方便,我用了dept
對象直接透傳,使用的是mybatis-plus
操作數(shù)據(jù)庫,可以替換成自己喜歡的ORM。
@Service("deptService")
public class DeptServiceImpl extends ServiceImpl<DeptDao, Dept> implements DeptService {
@Override
@Transactional(rollbackFor = Exception.class)
public void insert(Dept dept) {
// 如果有上級部門id,則獲取上級機(jī)構(gòu)
Dept parentDept = null;
if (dept.getParentId() != null) {
parentDept = this.getById(dept.getParentId());
// 上級機(jī)構(gòu)不能為空
if (parentDept == null) {
throw new RuntimeException("上級機(jī)構(gòu)不存在");
}
}
// MybatisPlus新增后可以獲取主鍵
this.save(dept);
// 更新樹路徑
if (parentDept != null) {
dept.setTreePath(parentDept.getTreePath() + dept.getId() + "/");
} else {
dept.setTreePath("/" + dept.getId() + "/");
}
this.updateById(dept);
}
}
3.2.數(shù)據(jù)更新
數(shù)據(jù)更新需要注意兩個點(diǎn):
- 新的上級部門不能是自己,也不能是自己的子部門(避免成環(huán))。
- 更新樹路徑之后,樹路徑上的所有子部門都需要更新樹路徑。
@Override
@Transactional(rollbackFor = Exception.class)
public void update(Dept dept) {
Dept newParentDept = null;
if (dept.getParentId() != null) {
newParentDept = this.getById(dept.getParentId());
if (newParentDept == null) {
throw new RuntimeException("上級部門不存在");
}
if (newParentDept.getTreePath().contains("/" + dept.getId() + "/")) {
throw new RuntimeException("上級部門不能是自己或子部門");
}
}
this.updateById(dept);
// 組裝新的樹路徑
String newTreePath = (newParentDept == null ? "" : newParentDept.getTreePath()) + dept.getId() + "/"; + dept.getId() + "/";
// 獲取原有的樹路徑
String oldTreePath = this.getById(dept.getId()).getTreePath();
// 獲取所有子部門(循環(huán)更新也可以替換為使用Mysql的replace函數(shù)批量更新)
LambdaQueryWrapper<Dept> queryWrapper = new LambdaQueryWrapper<>();
// likeRight表示右模糊查詢,即以oldTreePath開頭的
queryWrapper.likeRight(Dept::getTreePath, oldTreePath);
this.list(queryWrapper).forEach(childDept -> {
// 更新子部門的樹路徑
childDept.setTreePath(childDept.getTreePath().replace(oldTreePath, newTreePath));
this.updateById(childDept);
});
}
上面的循環(huán)更新在數(shù)據(jù)量不大的時候可以這么做,如果量較大的話,推薦使用mysql
中的replace
函數(shù)替換:
update dept set tree_path = replace(tree_path,'舊路徑','新路徑')
where tree_path like '舊路徑%'
把sql
中的舊路徑,新路徑替換為上面代碼中獲取到的路徑即可。
3.4.部門樹組裝
部門樹組裝只需要把需要組裝的部門列表查詢出來,然后根據(jù)parent_id
的關(guān)聯(lián)關(guān)系組裝數(shù)據(jù)即可。這里tree_path
就可以派上用場了,如果只有parent_id
的話,要么必須全量查詢所有的部門再過濾,要么需要根據(jù)parent_id
做遞歸查詢,而通過tree_path
可以直接做右模糊查詢,查詢到的部門都是需要的部門。
我們可以在接口中接收一個部門的id,把這個部門作為部門子樹的根節(jié)點(diǎn):
@Override
public List<DeptNode> tree(Long id) {
// 傳入了主鍵id,則通過主鍵id對于treePath做右模糊查詢,沒有傳入主鍵id,則查詢所有
List<Dept> list;
if (id != null) {
Dept baseDept = this.getById(id);
list = this.list(new LambdaQueryWrapper<Dept>().likeRight(Dept::getTreePath, baseDept.getTreePath()));
} else {
list = this.list();
}
// 將Dept轉(zhuǎn)換為DeptNode
List<DeptNode> deptNodes = new ArrayList<>();
for (Dept dept : list) {
DeptNode deptNode = BeanUtil.copyProperties(dept, DeptNode.class);
deptNode.setLabel(dept.getName());
deptNodes.add(deptNode);
}
// 循環(huán)遍歷,將子節(jié)點(diǎn)放入父節(jié)點(diǎn)的children中
for (DeptNode node : deptNodes) {
deptNodes.stream().filter(item -> node.getId().equals(item.getParentId())).forEach(item -> {
if (node.getChildren() == null) {
node.setChildren(CollUtil.newArrayList(item));
} else {
node.getChildren().add(item);
}
});
}
// 返回根節(jié)點(diǎn)
return deptNodes.stream()
.filter(item -> item.getParentId() == null || item.getId().equals(id))
.collect(Collectors.toList());
}
4.測試
通過一個Controller
接口發(fā)起測試:
@RestController
@RequestMapping("dept")
public class DeptController {
@Resource
private DeptService deptService;
@PostMapping("insert")
public void insert(@RequestBody @Valid Dept dept) {
this.deptService.insert(dept);
}
@PostMapping("update")
public void update(@RequestBody @Valid Dept dept) {
this.deptService.update(dept);
}
@PostMapping("/tree")
public List<DeptNode> tree(Long id) {
return this.deptService.tree(id);
}
}
4.1.部門新增
按照下面的請求參數(shù)順序發(fā)起insert
請求,為了驗證的方便,這里的部門加了數(shù)字后綴:
{
"parentId": null,
"name": "根部門"
}
{
"parentId": 1,
"name": "一級部門-1"
}
{
"parentId": 1,
"name": "一級部門-2"
}
{
"parentId": 2,
"name": "二級部門-1-1"
}
{
"parentId": 3,
"name": "二級部門-2-1"
}
{
"parentId": 5,
"name": "三級部門-2-1-1"
}
{
"parentId": 5,
"name": "三級部門-2-1-2"
}
執(zhí)行后數(shù)據(jù)的結(jié)果如下,我們可以看到tree_path
已經(jīng)正常添加好了:
通過tree
接口,不傳id獲取到的樹結(jié)構(gòu)如下,按照上面說的部門后綴進(jìn)行對比驗證,可以看出部門樹已經(jīng)正確組裝了。
[
{
"children": [
{
"children": [
{
"children": [],
"id": 4,
"parentId": 2,
"label": "二級部門-1-1",
"treePath": "/1/2/4/"
}
],
"id": 2,
"parentId": 1,
"label": "一級部門-1",
"treePath": "/1/2/"
},
{
"children": [
{
"children": [
{
"children": [],
"id": 6,
"parentId": 5,
"label": "三級部門-2-1-1",
"treePath": "/1/3/5/6/"
},
{
"children": [],
"id": 7,
"parentId": 5,
"label": "三級部門-2-1-2",
"treePath": "/1/3/5/7/"
}
],
"id": 5,
"parentId": 3,
"label": "二級部門-2-1",
"treePath": "/1/3/5/"
}
],
"id": 3,
"parentId": 1,
"label": "一級部門-2",
"treePath": "/1/3/"
}
],
"id": 1,
"parentId": null,
"label": "根部門",
"treePath": "/1/"
}
]
4.2.部門修改
假設(shè)現(xiàn)在我想把二級部門-2-1
直接掛接到根部門下,則兩個三級部門也會跟著一起遷移,嘗試一下做這個修改,請求參數(shù)如下:
{
"id": 5,
"parentId": null,
"name": "二級部門-2-1(改)"
}
執(zhí)行后,數(shù)據(jù)庫的結(jié)果如下,tree_path
中間的/3/
已經(jīng)去掉了:
4.3.子樹查詢
傳入二級部門-2-1(改)
的id,查詢子樹,期望可以返回三個部門,一個父部門,兩個子部門,請求tree
接口的結(jié)果與期望相符:文章來源:http://www.zghlxwxcb.cn/news/detail-706999.html
[
{
"children": [
{
"children": [],
"id": 6,
"parentId": 5,
"label": "三級部門-2-1-1",
"treePath": "/1/5/6/"
},
{
"children": [],
"id": 7,
"parentId": 5,
"label": "三級部門-2-1-2",
"treePath": "/1/5/7/"
}
],
"id": 5,
"parentId": 1,
"label": "二級部門-2-1(改)",
"treePath": "/1/5/"
}
]
5.結(jié)語
通過組合模式加上一點(diǎn)數(shù)據(jù)庫的設(shè)計,可以實現(xiàn)大部分常規(guī)的樹狀結(jié)構(gòu)的需求,希望對大家能有所幫助。文章來源地址http://www.zghlxwxcb.cn/news/detail-706999.html
到了這里,關(guān)于【設(shè)計模式】組合模式實現(xiàn)部門樹實踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!