1. 程式人生 > >Spring Boot專案通用功能第二講之《樹結構》

Spring Boot專案通用功能第二講之《樹結構》

前言

接上一篇文章中我們說了下怎麼去做《通用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類實現給介面就可以了,不會關注你的具體欄位名稱是什麼。

最後

下一篇我們接著講通用服務的封裝,下一篇講解通用屬性服務。

歡迎關注我們的公眾號或加群,等你哦!