一、前言
????在后臺代碼的開發(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的冗余字段將變的非常之多。