前言

本章以下小节主要介绍了部门岗位的代码生成和配置操作。

  • 代码生成。
  • 基础配置。

最后几小节则主要介绍了橙单部门结构的技术实现和优化策略。

  • 部门模型设计。
  • 部门代码实现。

代码生成

在橙单代码生成器的「应用列表」中,编辑应用的后台配置选项。需要注意的是,仅当支持部门时,才能选择支持岗位。

基础配置

下面主要介绍在日常开发中经常用到的场景配置。

岗位配置

岗位中包含是否「领导岗位」的标记,通常而言,岗位与部门是组合使用的,即部门岗位。属于领导岗位的用户,可被视为该部门的领导。这一点会在本章后面的小节进行介绍。

部门配置

橙单的部门是典型的树形结构,在下图所示页面完成部门的「增删改查」等管理操作。

下图是为部门设定岗位的操作。同一个部门可以包含多个岗位,而同一岗位也可以关联到多个不同的部门,因此部门与岗位之间是典型的多对多关联关系。

用户配置

在创建新用户时,需要为用户指定部门和岗位。属于带有「领导岗位」的用户,即可被视为该部门的领导。

在下图中需要特别强调的是,必须先选择部门,再选择与该部门关联的岗位数据。

部门领导

如下图所示,用户 leaderLaw 所属的岗位是「法务部经理」。由于该岗位为「领导岗位」,因此当前用户属于其所在部门「法务部」的领导。

上级部门领导

由下图可知,「法务部」的上级部门是「公司总部」。属于上级部门领导岗位的用户,即为上级部门领导。

因此,下图中的用户 leader 是上图 leaderLaw 用户的上级部门领导。

部门模型设计

橙单的树形部门实现机制,非常巧妙且高效,并极具编程技巧,阅读后您将会有如下收获。

  • 可深入了解多层级数据的最优化设计模型。
  • 以近乎完美的方式,实现了该设计模型的每一处代码细节。
  • 极为高效的批量插入 SQL 写法。
  • 触类旁通,该策略同样适用于其他数据量大,且层级深的树形结构数据的存储和检索优化。

传统实现方式

下面先介绍一下此前最常用的两种设计方式。

  • 部门表中包含部门 ID 和上级部门 ID 字段,当查询指定部门 ID 的所有层级子部门列表时,需利用数据库特有的递归查询 SQL 语句,获取查询结果,表结构如下图。

查询指定部门 ID 的所有层级子部门列表。这里仅以典型的 Oracle 递归查询语法为例,SQL 语句如下。

-- MySQL或PostgreSQL等其他数据库的写法,我也不太了解。
SELECT d.* FROM zz_sys_dept d START WITH d.dept_id = #{deptId} 
CONNECT BY PRIOR d.dept_id = d.parent_dept_id
  • 部门表中包含部门 ID 和上级部门 ID 字段,同时新增上级部门 ID 路径字段 (parent_id_path),并将所有上级部门 ID 存入该字段,部门 ID 之间使用分隔符隔开。查询指定部门 ID 的所有层级子部门列表时,使用 parent_id_path LIKE ‘%xxx%' 查询条件进行模糊搜索,表结构如下图。

查询指定部门 ID 的所有层级子部门列表,SQL 语句如下。

-- 该方式由于使用了 LIKE '%xxx%’,因此会抑制索引。
SELECT * FROM zz_sys_dept WHERE parent_id_path LIKE '%xxxxx%' AND dept_id = #{deptId}

优化后模型

在优化的道路上,空间换时间是横亘不变的策略之一。为了规避以上两种设计方式中存在的性能缺陷,我们新增了一张部门关联表,用于扁平化存储部门上下级之间的关联关系,表结构如下图。

上图中部门表 (zz_sys_dept),存储部门业务数据。而部门关联表 (zz_sys_dept_relation) ,会扁平化存储当前部门与所有下级子部门之间的关联关系数据。下面是部门表 zz_sys_dept 中存储的部门层级数据结构。

├── dept-one
│       └── dept-two-A
│              └── dept-three-A
│              └── dept-three-B
│       └── dept-two-B
│              └── dept-three-C
│              └── dept-three-D
│              └── dept-three-E

上面的树形部门结构,在部门关联表 zz_sys_dept_relation 中扁平化展开后的存储结构如下。我们不难发现,每个部门 ID 都会和他所有层级的子部门保持一条关联关系数据。

parentDeptId deptId
dept-one dept-one
dept-one  dept-two-A
dept-one  dept-three-A
dept-one  dept-three-B
dept-one dept-two-B
dept-one dept-three-C
dept-one dept-three-D
dept-one dept-three-E
dept-two-A dept-two-A
dept-two-A dept-three-A
dept-two-A dept-three-B
dept-two-B dept-two-B
dept-two-B dept-three-C
dept-two-B dept-three-D
dept-two-B dept-three-E
dept-three-A dept-three-A
dept-three-B dept-three-B
dept-three-C dept-three-C
dept-three-D dept-three-D
dept-three-E dept-three-E

看过上面的数据结构之后,再查询部门的所有层级子部门数据时,见如下 SQL。

-- 没有了递归查询,也不存在数据库兼容性问题了,而且两个表都有相关的索引,可以提升查询效率。
SELECT d.* FROM zz_sys_dept d, zz_sys_dept_relation r 
WHERE r.dept_id = d.dept_id AND r.parent_dept_id = #{deptId}

综合比对

  • 最简单表结构。
  • 部门表包含上级部门 ID 路径字段 (parent_id_path)。
  • 最优方式,新增部门关联表 (zz_sys_dept_relation)。
  递归查询 数据库语法 索引优化 实现难度 查询性能 层级变更成本
第一种方式 不兼容 具体看数据库实现
第二种方式 没有 全兼容 索引被抑制 极高
最优方式 没有 全兼容 索引优化 一般

部门代码实现

通过以上表格可以看到,最优化的设计模型,其代码实现难度相对较高。这主要体现在部门的「增删改」实现逻辑中,还需要同步维护 zz_sys_dept_relaiton 表中存储的部门上下级关联关系数据。

子部门查询

这里为了让本小节更为完整,我们再次给出,获取指定部门的多层级子部门的查询  SQL。从中可以看出,一次正常的数据表内关联查询,即可得到数据结果。查询过程中,索引也能被很好的利用。

SELECT d.* FROM zz_sys_dept d, zz_sys_dept_relation r 
WHERE r.dept_id = d.dept_id AND r.parent_dept_id = #{deptId}

新增部门

新增部门时,需要先在部门表 zz_sys_dept 中插入一条部门数据。然后再在同一事务内,在 zz_sys_dept_relation 表中同步添加,该部门 ID 和所有上级部门 ID 之间的关联数据,同时也要插入一条自己和自己的关联数据。

// 下面的代码来自于SysDeptServiceImpl.java
@Transactional(rollbackFor = Exception.class)
@Override
public SysDept saveNew(SysDept sysDept, SysDept parentSysDept) {
   // 这里会现在zz_sys_dept表中插入一条新的部门数据。
   sysDept.setDeptId(idGenerator.nextLongId());
   sysDept.setDeletedFlag(GlobalDeletedFlag.NORMAL);
   MyModelUtil.fillCommonsForInsert(sysDept);
   sysDeptMapper.insert(sysDept);
   if (parentSysDept == null) {
       // 如果新增的部门没有父部门,这里只需要在zz_sys_dept_relation插入一条自己和自己关联的记录。
       sysDeptRelationMapper.insert(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId()));
   } else {
       // 需要在zz_sys_dept_relation中,插入更多关联数据。该SQL的实现,详见下面的SQL片段。
       // 1. 父部门(parentSysDept.getDeptId)的所有上级部门Id,与当前部门(sysDept.getDeptId)的关联关系。
       // 2. 同时插入自己和自己的关联关系。
       sysDeptRelationMapper.insertParentList(parentSysDept.getDeptId(), sysDept.getDeptId());
   }
   return sysDept;
}

下面是 SysDeptRelationMapper.xml 中的 SQL 片段,写的「非常非常出色」。足以见得橙单开发者们的编程能力,以及对待技术问题追求极致最优解的工匠之心。因此花点儿时间,彻底理解下面的代码,绝对不虚此行。

-- 批量插入新增的部门与其所有上级部门的关联关系。
INSERT INTO zz_sys_dept_relation(parent_dept_id, dept_id)
   -- t.parent_dept_id 是当前新增部门的所有上级部门Id。
   -- 而变量#{deptId},就是新增的部门Id。
   -- 从而在这个SELECT中,将直接计算出当前新增部门Id和其所有上级部门Id的关联数据列表。
   SELECT t.parent_dept_id, #{myDeptId} 
   FROM zz_sys_dept_relation t
   -- 下面的条件将过滤出指定部门的所有父部门列表。
   -- 而这里指定的部门变量#{parentDeptId},是当前新增部门的父部门Id。
   -- 这样的查询结果就将返回该父部门的所有上级部门,同时也包含自己(parentDeptId)
   WHERE t.dept_id = #{parentDeptId}
   UNION ALL
   -- union all 一下自己和自己关系。这样就可以在一条SQL中完成,减少了数据库和服务之间的网络开销。
   SELECT #{myDeptId}, #{myDeptId}

删除部门

删除部门时,先从部门表 zz_sys_dept 中删除该部门数据,同时再在同一事务内,同步删除 zz_sys_dept_relation 表中,所有与该部门 ID 关联的数据,被删除的关联关系数据,均为与当前部门 ID 关联的上级部门 ID。

// 下面的代码来自于SysDeptServiceImpl.java
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Long deptId) {
   // 先从部门表中删除当前部门Id。
   if (sysDeptMapper.deleteById(deptId) == 0) {
       return false;
   }
   // 这里删除当前部门Id与其所有父部门Id的关联关系数据。
   // 当前部门和子部门的关系无需在这里删除,因为包含子部门时不能删除父部门。
   SysDeptRelation deptRelation = new SysDeptRelation();
   deptRelation.setDeptId(deptId);
   sysDeptRelationMapper.delete(new QueryWrapper<>(deptRelation));
   return true;
}

更新部门层级

更新部门时,如果没有涉及到部门层级的变化 (parent_id不变),只需在 zz_sys_dept 表中直接更新部门数据即可。否则,就需要在同一事务,同步修改 zz_sys_dept_relation 表中,所有与该部门 ID 关联的上下级部门关联关系数据。这一逻辑的代码实现相对复杂,我们尽量在以下的代码中,给出更为详细的注释。

// 下面的代码来自于SysDeptServiceImpl.java
@Transactional(rollbackFor = Exception.class)
@Override
public boolean update(SysDept sysDept, SysDept originalSysDept) {
   MyModelUtil.fillCommonsForUpdate(sysDept, originalSysDept);
   UpdateWrapper<SysDept> uw = this.createUpdateQueryForNullValue(sysDept, sysDept.getDeptId());
   // 先在zz_sys_dept表中,更新部门的业务数据。
   if (sysDeptMapper.update(sysDept, uw) == 0) {
       return false;
   }
   // 判断部门的层级是否变化,如果变化了,就需要在zz_sys_dept_relation中,先移除该部门Id
   // 与原上级部门Id之间的关联关系,以及该部门的所有子部门,与当前部门原上级部门Id之间的关联关系,
   // 再重新计算并保存,当前部门及其子部门,与新父部门Id列表之间的关联关系。
   if (ObjectUtils.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) {
       this.updateParentRelation(sysDept, originalSysDept);
   }
   return true;
}

部门层级关系变化的代码是最为复杂的,所以我们放到了本小节的最后才给出,如果您已经理解了前面讲述的部门数据关系模型,以及部门新增和删除中的代码逻辑,那么对于以下代码的理解,将会更为容易。

private void updateParentRelation(SysDept sysDept, SysDept originalSysDept) {
   List<Long> originalParentIdList = null;
   // 1. 因为层级关系变化了,所以要先遍历出,当前部门的原有父部门Id列表。
   if (originalSysDept.getParentId() != null) {
       LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
       queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getDeptId());
       List<SysDeptRelation> relationList = sysDeptRelationMapper.selectList(queryWrapper);
       originalParentIdList = relationList.stream()
               .filter(c -> !c.getParentDeptId().equals(sysDept.getDeptId()))
               .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList());
   }
   // 2. 毕竟当前部门的上级部门变化了,所以当前部门和他的所有子部门,与当前部门的原有所有上级部门
   // 之间的关联关系就要被移除。
   // 这里先移除当前部门的所有子部门,与当前部门的所有原有上级部门之间的关联关系。
   if (CollUtil.isNotEmpty(originalParentIdList)) {
       sysDeptRelationMapper.removeBetweenChildrenAndParents(originalParentIdList, sysDept.getDeptId());
   }
   // 这里更进一步,将当前部门Id与其原有所有上级部门Id之间的关联关系删除。
   SysDeptRelation filter = new SysDeptRelation();
   filter.setDeptId(sysDept.getDeptId());
   sysDeptRelationMapper.delete(new QueryWrapper<>(filter));
   // 3. 重新计算当前部门的新上级部门列表。
   List<Long> newParentIdList = new LinkedList<>();
   // 这里要重新计算出当前部门所有新的上级部门Id列表。
   if (sysDept.getParentId() != null) {
       LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
       queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getParentId());
       List<SysDeptRelation> relationList = sysDeptRelationMapper.selectList(queryWrapper);
       newParentIdList = relationList.stream()
               .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList());
   }
   // 4. 先查询出当前部门的所有下级子部门Id列表。
   LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
   queryWrapper.eq(SysDeptRelation::getParentDeptId, sysDept.getDeptId());
   List<SysDeptRelation> childRelationList = sysDeptRelationMapper.selectList(queryWrapper);
   // 5. 将当前部门及其所有子部门Id与其新的所有上级部门Id之间,建立关联关系。
   List<SysDeptRelation> deptRelationList = new LinkedList<>();
   deptRelationList.add(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId()));
   for (Long newParentId : newParentIdList) {
       deptRelationList.add(new SysDeptRelation(newParentId, sysDept.getDeptId()));
       for (SysDeptRelation childDeptRelation : childRelationList) {
           deptRelationList.add(
                   new SysDeptRelation(newParentId, childDeptRelation.getDeptId()));
       }
   }
   // 6. 执行批量插入SQL语句,插入当前部门Id及其所有下级子部门Id,与所有新上级部门Id之间的关联关系。
   sysDeptRelationMapper.insertList(deptRelationList);
}

结语

赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。