一條語句進(jìn)行建樹

一、前言

????在后臺代碼的開發(fā)中端壳,經(jīng)常碰到有父子關(guān)聯(lián)的表設(shè)計(jì)告丢,前端要求在后臺數(shù)據(jù)以樹形封裝進(jìn)行返回,因此如何建樹损谦,或者如何更優(yōu)雅的建樹成了后臺開發(fā)工作中繞不過去的一個(gè)坎岖免,根據(jù)工作所需,這里做下總結(jié)照捡,以堪后由颅湘。

(推薦方式二)

二、方法

1.方式一:獨(dú)立對象建樹

1.1 封裝獨(dú)立對象TreeNode

package cn.keyidea.common.bean;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.util.List;

/**
 * 樹節(jié)點(diǎn)實(shí)體
 */
@Data
@Schema(name = "TreeNode", description = "樹節(jié)點(diǎn)實(shí)體")
public class TreeNode {

    @Schema(description = "節(jié)點(diǎn)id", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer id;

    @Schema(description = "節(jié)點(diǎn)標(biāo)題", requiredMode = Schema.RequiredMode.REQUIRED)
    private String title;

    @Schema(description = "子節(jié)點(diǎn)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private List<TreeNode> children;

    @Schema(description = "父節(jié)點(diǎn)id", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer parentId;

    /**
     * 節(jié)點(diǎn)是否初始為選中狀態(tài)(如果開啟復(fù)選框的話)栗精,默認(rèn) false
     */
    @Schema(description = "節(jié)點(diǎn)是否初始為選中狀態(tài)(如果開啟復(fù)選框的話)闯参,默認(rèn) false", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private Boolean checked;

    @Schema(description = "URL地址", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String url;

    @Schema(description = "類型", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private Integer type;

    @Schema(description = "code碼", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String code;

    @Schema(description = "圖標(biāo)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String icon;

    @Schema(description = "路由地址", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String component;
}

1.2 建樹工具類

package cn.keyidea.common.util;

import cn.keyidea.common.bean.TreeNode;

import java.util.ArrayList;
import java.util.List;

/**
 * Tree工具類
 */
public class TreeUtil {
    /**
     * 兩層循環(huán)實(shí)現(xiàn)建樹
     *
     * @param treeNodes 傳入的樹節(jié)點(diǎn)列表
     * @return
     */
    public static List<TreeNode> bulidTree(List<TreeNode> treeNodes) {

        List<TreeNode> trees = new ArrayList<TreeNode>();

        for (TreeNode treeNode : treeNodes) {

            if (0 == treeNode.getParentId()) {
                trees.add(treeNode);
            }

            for (TreeNode it : treeNodes) {
                if (null != it.getParentId()) {
                    if (it.getParentId().equals(treeNode.getId())) {
                        if (treeNode.getChildren() == null) {
                            treeNode.setChildren(new ArrayList<TreeNode>());
                        }
                        treeNode.getChildren().add(it);
                    }
                }
            }
        }
        return trees;
    }
}

2.方式二:原生對象建樹(推薦)

2.1 以Entity實(shí)體作為自身節(jié)點(diǎn)

由于菜單一般設(shè)計(jì)成帶有父子項(xiàng),因此這里以菜單對象做示例悲立。

package cn.keyidea.sys.entity;

import cn.keyidea.common.annotation.EnumValue;
import cn.keyidea.common.bean.BaseModel;
import cn.keyidea.common.valid.GroupAdd;
import cn.keyidea.common.valid.GroupUpdate;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 系統(tǒng)菜單
 *
 * @author qyd
 * @date 2022-10-17
 */
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_menu")
@Schema(name = "SysMenu", description = "系統(tǒng)菜單")
@Entity
@Table(name = "sys_menu")
@Comment("系統(tǒng)菜單表")
public class SysMenu extends BaseModel {

    @NotNull(message = "父菜單不能為NULL", groups = {GroupAdd.class, GroupUpdate.class})
    @Schema(description = "父菜單ID鹿寨,一級菜單為0", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @Column(name = "parent_id", columnDefinition = "int(10) not null COMMENT '父菜單ID,一級菜單為0'")
    private Integer parentId;

    // name ==> label
    @NotBlank(message = "菜單名稱不能為空", groups = {GroupAdd.class, GroupUpdate.class})
    @Schema(description = "菜單名稱", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "label", columnDefinition = "varchar(100) not null COMMENT '菜單名稱'")
    private String label;

    @NotNull(message = "菜單類型不能為NULL", groups = {GroupAdd.class, GroupUpdate.class})
    @EnumValue(intValues = {0, 1, 2, 3}, message = "菜單類型不符合條件")
    @Schema(description = "菜單類型:0-系統(tǒng)薪夕,1-目錄脚草,2-菜單,3-按鈕", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "type", columnDefinition = "int(2) not null COMMENT '菜單類型:0-系統(tǒng)寥殖,1-目錄玩讳,2-菜單,3-按鈕'")
    private Integer type;

    @Schema(description = "菜單圖標(biāo)url", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "icon", columnDefinition = "varchar(100) COMMENT '菜單圖標(biāo)url'")
    private String icon;

    // url ==> key
    // 數(shù)據(jù)庫存儲url嚼贡,返回給前端使用key(key為MySQL關(guān)鍵字無法作為存儲字段)
    @JsonProperty("key")
    @Schema(description = "菜單URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "url", columnDefinition = "varchar(100) COMMENT '菜單URL'")
    private String url;

    @Schema(description = "菜單路由地址", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "component", columnDefinition = "varchar(100) COMMENT '菜單路由地址'")
    private String component;

    @Schema(description = "權(quán)限標(biāo)識熏纯,如:user:list", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "perms", columnDefinition = "varchar(100) COMMENT '權(quán)限標(biāo)識,如:user:list'")
    private String perms;

    @NotNull(message = "排序號不能為NULL", groups = {GroupAdd.class, GroupUpdate.class})
    @Schema(description = "排序號", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "order_num", columnDefinition = "int(10) not null COMMENT '排序號'")
    private Integer orderNum;

    @TableLogic
    @JsonIgnore
    @Column(name = "del_flag", columnDefinition = "int(2) not null COMMENT '刪除狀態(tài):0-正常粤策,1-刪除'")
    @TableField(value = "del_flag", fill = FieldFill.INSERT)
    private Integer delFlag;

    // ---------------------- 以下非數(shù)據(jù)庫字段 ----------------------

    @Schema(description = "子孩子", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @TableField(exist = false)
    @Transient
    private List<SysMenu> children;

    /**
     * 建樹
     *
     * @param list     待建樹集合
     * @param parentId 父節(jié)點(diǎn)樟澜,一般為0作為父節(jié)點(diǎn)
     * @return 返回樹
     */
    public static List<SysMenu> streamToTree(List<SysMenu> list, Integer parentId) {
        return list.stream()
                .filter(parent -> Objects.equals(parent.getParentId(), parentId))
                .peek(child -> child.setChildren(streamToTree(list, child.getId())))
                .collect(Collectors.toList());
    }
}

2.2 涉及BaseModel

package cn.keyidea.common.bean;

import cn.keyidea.common.valid.GroupChangePwd;
import cn.keyidea.common.valid.GroupCustomWithId;
import cn.keyidea.common.valid.GroupUpdate;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;


/**
 * 實(shí)體父類
 * <p>
 * 特殊說明:
 * 繼承此基類的實(shí)體類,如果使用lombok的@Data注解時(shí)叮盘,主要同時(shí)添加@EqualsAndHashCode(callSuper = true)注解
 * 1)注解@EqualsAndHashCode(callSuper = true)秩贰,就是用自己的屬性和從父類繼承的屬性來生成hashcode;
 * 2)注解@EqualsAndHashCode(callSuper = false)柔吼,就是只用自己的屬性來生成hashcode毒费;
 * 3)@Data相當(dāng)于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode這5個(gè)注解的合集, 和@EqualsAndHashCode默認(rèn)是false。
 * <p>
 * MappedSuperclass注解說明:
 * 1.@MappedSuperclass注解僅僅能標(biāo)準(zhǔn)在類上愈魏;這個(gè)注解標(biāo)識在父類上面的觅玻,用來標(biāo)識父類
 * 2.標(biāo)注為@MappedSuperclass的類將不是一個(gè)完整的實(shí)體類想际,他將不會映射到數(shù)據(jù)庫表,可是他的屬性都將映射到其子類的數(shù)據(jù)庫字段中
 * 3.標(biāo)注為@MappedSuperclass的類不能再標(biāo)注@Entity或@Table注解
 * <p>
 * EntityListeners注解及相關(guān)說明:
 * EntityListeners:該注解用于指定Entity或者superclass上的回調(diào)監(jiān)聽類
 * AuditingEntityListener:這是一個(gè)JPA Entity Listener溪厘,用于捕獲監(jiān)聽信息胡本,當(dāng)Entity發(fā)生持久化和更新操作時(shí)
 *
 * @author qyd
 * @date 2022-10-17
 */
@SuperBuilder   // @SuperBuilder支持對基類成員屬性的構(gòu)造;如果子類繼承了BaseModel畸悬,也需要使用該注解
@AllArgsConstructor // 全參構(gòu)造函數(shù)
@NoArgsConstructor  // 空參構(gòu)造函數(shù)
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Schema(name = "BaseModel", description = "實(shí)體父類")
public class BaseModel implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    @NotNull(message = "ID不能為NUll", groups = {GroupUpdate.class, GroupChangePwd.class, GroupCustomWithId.class})
    @Schema(description = "主鍵ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Id
    @Column(name = "id", columnDefinition = "int(10) COMMENT '自增長ID'")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Schema(description = "創(chuàng)建時(shí)間", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "create_time", updatable = false, columnDefinition = "datetime not null COMMENT '創(chuàng)建時(shí)間'")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    @Schema(description = "創(chuàng)建人ID", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "create_by", updatable = false, columnDefinition = "int(10) not null COMMENT '創(chuàng)建人ID'")
    @TableField(value = "create_by", fill = FieldFill.INSERT)
    private Integer createBy;

    @Schema(description = "更新時(shí)間", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "update_time", columnDefinition = "datetime COMMENT '更新時(shí)間'")
    @TableField(value = "update_time", fill = FieldFill.UPDATE)
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    @Schema(description = "更新人ID", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "update_by", columnDefinition = "int(10) COMMENT '更新人ID'")
    @TableField(value = "update_by", fill = FieldFill.UPDATE)
    private Integer updateBy;

}

3.方式一 vs 方式二

比較項(xiàng) 方式一 方式二
簡潔度 優(yōu)
是否支持對象原生字段 優(yōu)
是否需要新增字段 部分
是否有冗余字段 存在
是否需要新建方法 不需要 需要

對于方式二來說侧甫,新增的字段主要就是在原生對象中新增private List<T> children;, 然后新增靜態(tài)方法streamToTree蹋宦,并且能完美支持原生所有字段披粟,所差的是,如果有10個(gè)類都需要建樹的話妆档,那么每個(gè)類都需要寫同樣的方法僻爽。不過對于方式一來說,如果真存在10個(gè)類贾惦,并且每個(gè)類字段都存在差異性胸梆,那么方式一中的TreeNode的冗余字段將變的非常之多。

END

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末须板,一起剝皮案震驚了整個(gè)濱河市碰镜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌习瑰,老刑警劉巖绪颖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異甜奄,居然都是意外死亡柠横,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門课兄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牍氛,“玉大人,你說我怎么就攤上這事烟阐“峥。” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵蜒茄,是天一觀的道長唉擂。 經(jīng)常有香客問我,道長檀葛,這世上最難降的妖魔是什么玩祟? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮屿聋,結(jié)果婚禮上卵凑,老公的妹妹穿的比我還像新娘庆聘。我一直安慰自己,他們只是感情好勺卢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著象对,像睡著了一般黑忱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勒魔,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天甫煞,我揣著相機(jī)與錄音,去河邊找鬼冠绢。 笑死抚吠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的弟胀。 我是一名探鬼主播楷力,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼孵户!你這毒婦竟也來了萧朝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤夏哭,失蹤者是張志新(化名)和其女友劉穎检柬,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖配,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡何址,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了进胯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片用爪。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖龄减,靈堂內(nèi)的尸體忽然破棺而出项钮,到底是詐尸還是另有隱情,我是刑警寧澤希停,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布烁巫,位于F島的核電站,受9級特大地震影響宠能,放射性物質(zhì)發(fā)生泄漏亚隙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一违崇、第九天 我趴在偏房一處隱蔽的房頂上張望阿弃。 院中可真熱鬧诊霹,春花似錦、人聲如沸渣淳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽入愧。三九已至鄙漏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棺蛛,已是汗流浹背怔蚌。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旁赊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓终畅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親声离。 傳聞我的和親對象是個(gè)殘疾皇子芒炼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容