前言
接上一篇文章中我们说了下怎么去做《通用service》,来简化单表操作下的通用service层的逻辑,今天我们来接着讲解下通用的树表结构操作。
思考
首先我们先思考一下,通用的树结构操作都需要那些功能?
对于树结构首先我们知道该表一定是一个自关联的,也就是需要一个关联自己的父ID,来做上下级关联,然后我们需要一个排序值,因为通常我们都需要对孩子节点有个排序,这样也方便使用,当然这个排序值是可有可无的,要看具体业务,好了我们的需求就是这样,下面我们来看具体的实现思路
实现上述思考
我们首先要确定的是我们的表结构,首先要求表中有个parent_id字段,作为自身的关联,还有一个sort字段(当然这个字段是可有可无的,根据业务需要)
1. 接下来我们需要两个接口类:TreePO、SortTreePO一个定义树接口另一个用作排序树,具体的业务实体对象应该实现该接口以取得通用树操作功能。
2. 还有个Node树节点类用来封装整颗树。
3. 在定义一个TreeCrudService树操作接口,该接口拥有操作树的常用方法定义,它的实现有两个:BaseTreeCurdServiceImpl、BaseSortTreeCrudServiceImpl,看名称大家也能够理解,一个实现了树的基本操作,另一个则再此基础上增加排序功能,如果你的业务不需要有排序字段,则继承第一个就可以了。
好了,举一个业务中的例子来说明下,加深一下对该功能的使用,我们现在以组织架构为例,组织架构中会存储:公司、部门、组别等信息,一个公司有多个部门,一个部门有多个组别,所以我们将其建立到一张表上,它其实是满足树型结构的,最后时我们用该具体的业务例子向你演示一下通用树操作该怎样实现以及使用,下面看下建表SQL语句:
-- 组织架构表
DROP TABLE IF EXISTS `org`;
CREATE TABLE `org` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL COMMENT '组织架构名称',
`type` varchar(32) NOT NULL COMMENT '类型',
`sort` int(11) DEFAULT 0 COMMENT '排序值',
`parent_id` int(11) NOT NULL COMMENT '父ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX index_parent_id(`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='组织架构';
TreePO树实体父接口:
package com.zm.zhuma.commons.model.po;
public interface TreePO<PK> extends PO<PK> {
PK getParentId();
void setParentId(PK parentId);
}
树节点:
package com.zm.zhuma.commons.model.bo;
import com.zm.zhuma.commons.model.po.TreePO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @desc 树节点
*
* @author zhuamer
* @since 19/12/2017 9:54 AM
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Node<E extends TreePO> {
private E parent;
private List<Node<E>> children;
}
备注
- PO类(也就是数据库表对应的实体类)的统一接口,所有的PO类都应该实现该接口。
- 我们的实体类目录会分的相对详细些:po(persistant object 持久对象)、qo(query object查询对象)、vo(view object 值对象)、bo(business object 业务对象)。
TreeCrudService接口:
package com.zm.zhuma.commons.service;
import com.zm.zhuma.commons.model.po.TreePO;
/**
* @desc 树结构crud服务
*
* @author zhumaer
* @since 10/18/2017 18:31 PM
*/
public interface TreeCrudService<E extends TreePO, PK> extends
CrudService<E, PK>,
TreeSelectService<E, PK> {
}
package com.zm.zhuma.commons.service;
import com.zm.zhuma.commons.model.bo.Node;
import com.zm.zhuma.commons.model.po.TreePO;
import java.util.List;
/**
* @desc 树结构查看服务
*
* @author zhumaer
* @since 10/18/2017 18:31 PM
*/
public interface TreeSelectService<E extends TreePO, PK> {
/**
* 根据父节点id获取子节点数据
*
* @param parentId 父节点ID
* @return 子节点数据
*/
List<E> selectChildren(PK parentId);
/**
* 获取当前节点下树数据
*
* @param parentId 父节点ID
* @return 树信息
*/
Node<E> selectNodeByParentId(PK parentId);
}
备注
- TreeCrudService该服务,我们继承了CrudService,让其拥有普通表的全部增删改查功能,然后用E extends TreePO用以限制使用该接口服务,必须先有个parentId的功能实现。
- 对于树接口的查询我们定义两个方法一个获取孩子列表的,一个是获取完整树的。
BaseTreeCurdServiceImpl通用接口实现逻辑:
package com.zm.zhuma.commons.service.impl;
import com.google.common.collect.Lists;
import com.zm.zhuma.commons.model.bo.Node;
import com.zm.zhuma.commons.model.po.TreePO;
import com.zm.zhuma.commons.service.TreeCrudService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public abstract class BaseTreeCurdServiceImpl<E extends TreePO<PK>, PK> extends BaseMySqlCrudServiceImpl<E, PK> implements TreeCrudService<E, PK> {
private static int MAX_TREE_HIGH = 10;
@Override
public List<E> selectChildren(PK parentId) {
Assert.notNull(parentId, "parentId is null");
try {
E e = poType.newInstance();
e.setParentId(parentId);
return crudMapper.select(e);
} catch (InstantiationException | IllegalAccessException e) {
log.error("selectChildren occurs error, caused by: ", e);
throw new RuntimeException("selectChildren occurs error", e);
}
}
@Override
public Node<E> selectNodeByParentId(PK parentId) {
Assert.notNull(parentId, "parentId is null");
int currentTreeHigh = 1;
Node<E> tree = new Node<>();
E parent = super.selectByPk(parentId);
if (parent != null) {
Node<E> eNode = wrapNode(parent);
tree = buildTree(eNode, currentTreeHigh);
}
return tree;
}
private Node<E> buildTree(Node<E> eNode, int currentTreeHigh) {
if (currentTreeHigh++ >= MAX_TREE_HIGH) {
return eNode;
}
List<Node<E>> descendantNodes = getDescendantNodes(eNode.getParent().getId());
List<Node<E>> children = eNode.getChildren() == null ? Lists.newArrayList() : eNode.getChildren();
children.addAll(descendantNodes);
eNode.setChildren(children);
for (Node<E> node : descendantNodes) {
buildTree(node, currentTreeHigh);
}
return eNode;
}
private List<Node<E>> getDescendantNodes(PK id) {
List<E> eList = this.selectChildren(id);
List<Node<E>> list = Lists.newLinkedList();
for (E parent : eList) {
Node<E> node = wrapNode(parent);
list.add(node);
}
return list;
}
private Node<E> wrapNode(E parent) {
Node<E> node = new Node<>();
node.setParent(parent);
return node;
}
}
解释说明
- 获取整棵树功能算是本篇核心逻辑了,我们这里使用了递归去查询树。
- 这里我们定义了一个最大数深度,来限制查询树的最大层级(MAX_TREE_HIGH), 以防止库中数据存在环问题,导致的递归死循环。
- 在使用时,你只需让你的业务类继承该抽象类,就拥有树操作功能啦,下面我们再次把排序功能加入进来。
- 你可能由于使用递归是否有性能问题,这里说明下,如果你的表数据量较小该服务直接可以使用,如果比较大或者希望给前端快速放回,那么最好再此基础上增加缓存功能。
SortTreePO排序树实体对象接口:
package com.zm.zhuma.commons.model.po;
public interface SortTreePO<PK> extends TreePO<PK>, Comparable<SortTreePO> {
Integer getSort();
void setSort(Integer sort);
@Override
default int compareTo(SortTreePO sortTree) {
if (sortTree == null) {
return -1;
}
return Integer.compare(getSort() == null ? 0 : getSort(), sortTree.getSort() == null ? 0 : sortTree.getSort());
}
}
BaseSortTreeCrudServiceImpl排序树实现类:
package com.zm.zhuma.commons.service.impl;
import com.zm.zhuma.commons.model.bo.Node;
import com.zm.zhuma.commons.model.po.SortTreePO;
import com.zm.zhuma.commons.util.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public abstract class BaseSortTreeCrudServiceImpl<E extends SortTreePO<PK>, PK> extends BaseTreeCurdServiceImpl<E, PK> {
@Override
public List<E> selectChildren(PK parentId) {
List<E> children = super.selectChildren(parentId);
return children.stream().sorted().collect(Collectors.toList());
}
@Override
public Node<E> selectNodeByParentId(PK parentId) {
Node<E> node = super.selectNodeByParentId(parentId);
sortChildrenNode(node);
return node;
}
private void sortChildrenNode(Node<E> node) {
if (node.getParent() != null && CollectionUtil.isNotEmpty(node.getChildren())) {
List<Node<E>> children = node.getChildren();
List<Node<E>> sortedChildren = children.stream().sorted((node1, node2) -> {
E e1 = node1.getParent();
E e2 = node2.getParent();
if (e1 == null || e2 == null) {
throw new NullPointerException();
}
return e1.compareTo(e2);
}).collect(Collectors.toList());
node.setChildren(sortedChildren);
sortedChildren.forEach(item -> sortChildrenNode(item));
}
}
}
解释说明
- 我们依旧使用递归方法来完成对整颗树结构的排序,在SortTreePO中我们已经对其实现了Comparable,sort值越小排名越靠前。
- 就是这么简单排序树功能加入进来了,下面我们以刚刚说的组织架构实例,来演示一下该功能的具体使用方法。
组织架构实体对象:
package com.zm.zhuma.user.model.po;
import com.zm.zhuma.commons.annotations.EnumValue;
import com.zm.zhuma.commons.model.po.BasePO;
import com.zm.zhuma.commons.model.po.BaseSortTreePO;
import com.zm.zhuma.commons.validator.CreateGroup;
import com.zm.zhuma.commons.validator.UpdateGroup;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* @desc 组织架构实体
* @author zhumaer
* @since 6/15/2017 2:48 PM
*/
@ApiModel("组织架构实体")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Org extends BaseSortTreePO<Long> {
private static final long serialVersionUID = 2623905517895913619L;
@ApiModelProperty(value = "主键")
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
@ApiModelProperty(value = "组织架构名称")
@NotBlank(groups = CreateGroup.class)
@Length(min=1, max=64, groups = {CreateGroup.class, UpdateGroup.class})
private String name;
@ApiModelProperty(value = "类型")
@NotBlank(groups = CreateGroup.class)
@EnumValue(enumClass=TypeEnum.class, enumMethod="isValidName", groups = {CreateGroup.class, UpdateGroup.class})
private String type;
/**
* 组织架构类型枚举
*/
public enum TypeEnum {
/**公司*/
COMPANY,
/**部门*/
DEPARTMENT,
/**组别*/
GROUP;
public static boolean isValidName(String name) {
for (TypeEnum typeEnum : TypeEnum.values()) {
if (typeEnum.name().equals(name)) {
return true;
}
}
return false;
}
}
}
备注
- 这里我们继承BaseSortTreePO而没有去实现我们前面所说的SortTreePO,因为这个类里统一写了几个默认的字段类,以后我们就不用重复的在定义parentId、sort、createTime、updateTime这些通用字段了,如果你认为这样写不算特别合理,或者你的字段不想被上面字段名称所约束,就直接实现SortTreePO就好了。
- 因为篇幅有限,如想要该类源码,请到文章后面看github项目获取哈
组织架构mapper接口:
package com.zm.zhuma.user.service.mapper;
import com.zm.zhuma.commons.dao.CrudMapper;
import com.zm.zhuma.user.model.po.Org;
import org.springframework.stereotype.Repository;
@Repository
public interface OrgMapper extends CrudMapper<Org> {
}
组织架构服务接口:
package com.zm.zhuma.user.api;
import com.zm.zhuma.commons.service.TreeCrudService;
import com.zm.zhuma.user.model.po.Org;
/**
* 组织架构服务实现
* @author zhumaer
* @since 2018-5-22 10:58:51
*/
public interface OrgService extends TreeCrudService<Org, Long> {
}
组织架构服务接口实现:
package com.zm.zhuma.user.service.impl;
import com.zm.zhuma.commons.service.impl.BaseSortTreeCrudServiceImpl;
import com.zm.zhuma.user.api.OrgService;
import com.zm.zhuma.user.model.po.Org;
import org.springframework.stereotype.Service;
/**
* 组织架构服务实现
* @author zhumaer
* @since 2018-5-22 10:58:51
*/
@Service
public class OrgServiceImpl extends BaseSortTreeCrudServiceImpl<Org, Long> implements OrgService {
}
组织架构测试控制器:
package com.zhuma.demo.web.demo4;
import com.zm.zhuma.commons.model.bo.Node;
import com.zm.zhuma.commons.web.annotations.ResponseResult;
import com.zm.zhuma.user.client.OrgClient;
import com.zm.zhuma.user.model.po.Org;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @desc 组织架构管理控制器
*
* @author zhumaer
* @since 6/20/2017 16:37 PM
*/
@ResponseResult
@RestController("demo4OrgController")
@RequestMapping("demo4/orgs")
public class OrgController {
@Autowired
private OrgClient orgClient;
@PostMapping
Org add(@RequestBody Org org) {
Org dbOrg = orgClient.add(org);
return dbOrg;
}
@GetMapping("tree/{treeId}")
Node<Org> getTreeNode(@PathVariable("treeId") Long treeId) {
Node<Org> tree = orgClient.getTreeNode(treeId);
return tree;
}
}
成果展示
数据库中组织架构记录:
POST MAN接口调用截图:
响应结果展示:
{
"code": 1,
"msg": "成功",
"data": {
"parent": {
"id": 1,
"name": "筑码科技",
"type": "COMPANY",
"createTime": 1527001092000,
"updateTime": 1527001092000,
"parentId": 0
},
"children": [
{
"parent": {
"id": 3,
"name": "后端开发部",
"type": "DEPARTMENT",
"createTime": 1527001169000,
"updateTime": 1527002111000,
"parentId": 1,
"sort": 0
},
"children": []
},
{
"parent": {
"id": 2,
"name": "测试部",
"type": "DEPARTMENT",
"createTime": 1527001147000,
"updateTime": 1527001147000,
"parentId": 1,
"sort": 1
},
"children": [
{
"parent": {
"id": 8,
"name": "研发二组",
"type": "GROUP",
"createTime": 1527001279000,
"updateTime": 1527001279000,
"parentId": 2,
"sort": 99
},
"children": []
},
{
"parent": {
"id": 7,
"name": "研发一组",
"type": "GROUP",
"createTime": 1527001267000,
"updateTime": 1527001267000,
"parentId": 2,
"sort": 100
},
"children": []
},
{
"parent": {
"id": 9,
"name": "研发三组",
"type": "GROUP",
"createTime": 1527001289000,
"updateTime": 1527001289000,
"parentId": 2,
"sort": 101
},
"children": []
}
]
},
{
"parent": {
"id": 4,
"name": "H5开发部",
"type": "DEPARTMENT",
"createTime": 1527001178000,
"updateTime": 1527001178000,
"parentId": 1,
"sort": 2
},
"children": []
},
{
"parent": {
"id": 5,
"name": "ANDROID开发部",
"type": "DEPARTMENT",
"createTime": 1527001197000,
"updateTime": 1527001197000,
"parentId": 1,
"sort": 4
},
"children": []
},
{
"parent": {
"id": 6,
"name": "IOS开发部",
"type": "DEPARTMENT",
"createTime": 1527001209000,
"updateTime": 1527001209000,
"parentId": 1,
"sort": 5
},
"children": []
}
]
}
}
备注
- 到这里通用树操作功能基本上算是讲解完成啦,可能有些同学会有疑问,像上述这样写,会不会就限制了表中字段名一定是parent_id、sort字段,其实不是的,你可以看到我们的TreePO/SortTreePO都是接口,所以你只要保证你的PO类实现给接口就可以了,不会关注你的具体字段名称是什么。
最后
下一篇我们接着讲通用服务的封装,下一篇讲解通用属性服务。
附上本实例代码github地址(本实例在zhuma-demo项目下,demo4模块):https://github.com/zhumaer/zhuma
欢迎关注我们的公众号或加群,等你哦!