一. 背景
项目中往往会用到大量的树形结构查询以及删除,通过几个树形结构的操作之后,发现这部分的代码是可以抽象,部分接口是可以做到通过,做好这些,使得类似的操作可以得到简化,下面将讲解具体的操作过程。
二. 树查询
这里树分为干和枝,代码中相应的就是Node和TreeNode。
/**
* <p>
* 节点
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@Data
public class Node implements Serializable {
private static final long serialVersionUID = -3514061958234921653L;
/**
* id
*/
private Long id;
/**
* 名称
*/
private String nm;
}
/**
* <p>
* 树形接口节点
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class TreeNode<T> extends Node {
private static final long serialVersionUID = -8071463452448530550L;
/**
* 父id
*/
private Long pid;
/**
* 排序
*/
private int seq;
/**
* 层级
*/
private int level;
/**
* 子项集合
*/
private List<T> children;
public void addChild(T node) {
if (null == children) {
children = new ArrayList<>();
}
children.add(node);
}
}
以上两个类的树形和,基本就是我们需要的一些基本属性,如果需要其他属性,新建一个类继承TreeNode。比如RscoTreeNode该类就是继承TreeNode
/**
* <p>
* 授权资源树形结构类
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class RscoTreeNode extends TreeNode<RscoTreeNode> {
private static final long serialVersionUID = -8188319099997222199L;
private String url; //资源路径
private String perm; //权限标识
private String icon; //图标
private String rmks; //备注
private String catCd; //分类编码
private String catNm; //分类
private String statCd; //状态编码
private String statNm; //状态
}
有了这些代码相当于我们已经建了一棵树的模型,接下来就是怎么通过这些模型来建设树的过程。
新建一个TreeNodeUtils工具类,这里都是对dto进行操作,如果对dto不是很了解的查阅这个:https://blog.csdn.net/ssyujay/article/details/89025503( 查询结果dto,vo讲解)
/**
* <p>
* 树形结构生成工具
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
public class TreeNodeUtils {
/**
* 树形结构生成处理, vo对象将有Dto通过名称规范生成
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<T> nodeClass) {
return dtoListHandle(dtoList, null, nodeClass, null);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<?> voClass, Class<T> nodeClass) {
return dtoListHandle(dtoList, voClass, nodeClass, null);
}
/**
* 树形结构生成处理, vo对象将有Dto通过名称规范生成
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<T> nodeClass, Long pid) {
return dtoListHandle(dtoList, null, nodeClass, pid);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<?> voClass, Class<T> nodeClass, Long pid) {
if (CollectionUtils.isEmpty(dtoList)) {
return Collections.emptyList();
}
List<T> nodeList = new ArrayList<>();
for (D dto : dtoList) {
T node = ConvertUtils.convert(getVoByDto(dto, voClass), nodeClass);
nodeList.add(node);
}
return treeNodeHandle(nodeList, pid);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>> List<T> treeNodeHandle(List<T> nodeList) {
return treeNodeHandle(nodeList, null);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>> List<T> treeNodeHandle(List<T> nodeList, Long pid) {
if (CollectionUtils.isEmpty(nodeList)) {
return Collections.emptyList();
}
seqHandle(nodeList);
childFindParentHandler(nodeList);
List<T> resultList = new ArrayList<>();
if (null == pid) {
keepTrunk(resultList, nodeList, 1);
} else {
keepTrunk(resultList, nodeList, 1, pid);
}
return resultList;
}
/**
* 保留树形结构主干
*/
private static <T extends TreeNode<T>> List<T> keepTrunk(List<T> resultList,
List<T> nodeList, int level, long pid) {
for (T node : nodeList) {
if (null != node.getPid() && (pid == node.getPid().longValue() || level > 1)) {
if (pid == node.getPid().longValue()) {
resultList.add(node);
}
node.setLevel(level);
if (CollectionUtils.isNotEmpty(node.getChildren())) {
keepTrunk(resultList, node.getChildren(), level + 1, pid);
}
}
}
return resultList;
}
/**
* 保留树形结构主干
*/
private static <T extends TreeNode<T>> List<T> keepTrunk(List<T> resultList,
List<T> nodeList, int level) {
for (T node : nodeList) {
if (null == node.getPid() || level > 1) {
if (null == node.getPid()) {
resultList.add(node);
}
node.setLevel(level);
if (CollectionUtils.isNotEmpty(node.getChildren())) {
keepTrunk(resultList, node.getChildren(), level + 1);
}
}
}
return resultList;
}
/**
* 子项关联到父项,就关联到父项下面
*/
private static <T extends TreeNode<T>> void childFindParentHandler(List<T> nodeList) {
for (T node : nodeList) {
if (null != node.getPid()) {
T pNode = getTreeNode(nodeList, node.getPid());
if (null != pNode) {
pNode.addChild(node);
}
}
}
}
/**
* 获取项
*/
private static <T extends TreeNode<T>> T getTreeNode(List<T> nodeList, long id) {
for (T node : nodeList) {
if (id == node.getId().longValue()) {
return node;
}
}
return null;
}
/**
* 排序
*/
private static <T extends TreeNode<T>> List<T> seqHandle(List<T> nodeList) {
Collections.sort(nodeList, new Comparator<T>() {
@Override
public int compare(T o1, T o2) {
if (o1.getSeq() > o2.getSeq()) {
return 1;
}
return -1;
}
});
return nodeList;
}
/**
* 获取vo对象
*/
private static <D> Object getVoByDto(D dto, Class<?> voClass) {
String voName = null;
if (null != voClass) {
voName = voClass.getSimpleName();
} else {
Class<? extends Object> dtoClass = dto.getClass();
String simpleName = dtoClass.getSimpleName();
voName = simpleName.substring(0, simpleName.length() - 3) + "Vo";
}
Method method = ClassUtils.getMethod(dto.getClass(), "get"+ voName);
try {
return method.invoke(dto);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
下面将对TreeNodeUtils工具类中的一些方法使用进行讲解。(这里查询的对象都是dto,dto里面包含数据库映射对象实体vo)
/**
* 树形结构生成处理, vo对象将有Dto通过名称规范生成
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<T> nodeClass) {
return dtoListHandle(dtoList, null, nodeClass, null);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<?> voClass, Class<T> nodeClass) {
return dtoListHandle(dtoList, voClass, nodeClass, null);
}
这两个方法表示对查询dto列表进行属性结构处理,这里可以不传voClass,通过对dtoClass规则生成voClass。这里主要是对数据库对应的实体中id,pid,nm,seq等都与Node和TreeNode吻合可以使用该方法。且这里的顶级pid都是null。
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<T> nodeClass, Long pid) {
return dtoListHandle(dtoList, null, nodeClass, pid);
}
/**
* 树形结构生成处理, vo对象将有Dto通过名称规范生成
*/
public static <T extends TreeNode<T>, D> List<T> dtoListHandle(List<D> dtoList, Class<?> voClass, Class<T> nodeClass, Long pid) {
if (CollectionUtils.isEmpty(dtoList)) {
return Collections.emptyList();
}
List<T> nodeList = new ArrayList<>();
for (D dto : dtoList) {
T node = ConvertUtils.convert(getVoByDto(dto, voClass), nodeClass);
nodeList.add(node);
}
return treeNodeHandle(nodeList, pid);
}
这两个方法顶级父级pid不是null,所以需要把pid传进去进行过滤。
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>> List<T> treeNodeHandle(List<T> nodeList) {
return treeNodeHandle(nodeList, null);
}
/**
* 树形结构生成处理
*/
public static <T extends TreeNode<T>> List<T> treeNodeHandle(List<T> nodeList, Long pid) {
if (CollectionUtils.isEmpty(nodeList)) {
return Collections.emptyList();
}
seqHandle(nodeList);
childFindParentHandler(nodeList);
List<T> resultList = new ArrayList<>();
if (null == pid) {
keepTrunk(resultList, nodeList, 1);
} else {
keepTrunk(resultList, nodeList, 1, pid);
}
return resultList;
}
如果直接调用这两个方法,表示实体对象的dto和treeNode类中的属性不一致,需要程序额外处理在进行树形结构处理。比如如下:
@Override
public List<WxMenuItmTreeNode> listTreeNode(Long menuId) {
FindWrapper<SysWxMenuItmVo> fw = new FindWrapper<>();
fw.eq("menu_id", menuId);
fw.orderByAsc("seq");
List<SysWxMenuItmDto> list = this.list(fw);
if (CollectionUtils.isEmpty(list)) {
return Collections.emptyList();
}
List<WxMenuItmTreeNode> nodeList = new ArrayList<>();
for (SysWxMenuItmDto dto : list) {
WxMenuItmTreeNode node = ConvertUtils.convert(dto.getSysWxMenuItmVo(), WxMenuItmTreeNode.class);
node.setNm(dto.getSysWxMenuItmVo().getName());
node.setKey(dto.getSysWxMenuItmVo().getKe());
nodeList.add(node);
}
return TreeNodeUtils.treeNodeHandle(nodeList);
}
对于属性一致的可以直接使用公共接口进行查询(建表的时候,建议使用treeNode相同的属性)
@Override
public <T extends TreeNode<T>> List<T> listTreeNode(Wrapper<V> wrapper, Class<T> treeNodeClass) {
List<D> list = this.list(wrapper);
return TreeNodeUtils.dtoListHandle(list, treeNodeClass);
}
TreeNodeUtils中的其他方法就是对树形结构属性对象进行属性结构处理,使其成为一课树。
三. 树删除
/**
* <p>
* 删除(根据ID 删除树形结构数据)
* </p>
*
* @param id 主键ID
* @param pidColumn 父节点主键
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteTreeById(Serializable id, String pidColumn) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(currentModelClass());
Map<String, Object> columnMap = new HashMap<>();
columnMap.put(pidColumn, id);
List<Map<String, Object>> listMaps = this.listMapsByMap(columnMap);
if (CollectionUtils.isNotEmpty(listMaps)) {
for (Map<String, Object> map : listMaps) {
deleteTreeById((Serializable) map.get(tableInfo.getKeyColumn()), pidColumn);
}
}
this.deleteById(id);
}
/**
* <p>
* 删除(根据ID 批量删除树形结构数据)
* </p>
*
* @param idList 主键ID列表(不能为 null 以及 empty)
* @param pidColumn 父节点主键
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteTreeByIds(Collection<? extends Serializable> idList, String pidColumn) {
for (Serializable id : idList) {
deleteTreeById(id, pidColumn);
}
}
使用通用方法进行树形节点删除,pidColumn表示数据库父类主键值。