我正在参与「启航计划」

背景

公司首要做数据治理相关的产品,怎么办初期为了寻求速度留下了一些饮鸩止渴的规划,后期影响最大的便是文件夹这块,治理数据首先要归档数据,经过文件夹的办法来将同一事务特点的数据收拢到一同。

前期的事务逻辑简略,简略的结构足以满意文件夹树查询、查找等等需求,但发展到后期越来越多的满意文件夹结构但又不是文件夹的操作添加,比方分类、比方主题等等虽然有父子级联系但数据表不一致的情况。

【代码结构设计】优化重构文件夹的树状结构

导致前后端交互上同一个事务逻辑产生不同的数据结构,并且接口也不一致,有些在python服务上,有些在java服务上,每次开发都要写许多重复逻辑的代码复用性很低不通用。

于是乎,高层在某天召开会议,会议内容首要确认全面深化改革文件夹功用,并提出三个全面

  • 全面笼统实体
  • 全面建造低耦合、高内聚、易扩展接口
  • 全面优化加快接口速度

作为新时代夹娃工程师,一定不能孤负安排上交给我的任务,虽然我深知越厌恶的功用重构起来坑越多,但这些都难不倒勤劳勇敢的打工人(领导容许搞好给高绩效)。

【代码结构设计】优化重构文件夹的树状结构

在之前的代码上优化属于是屎上雕花了,革新就要流血,这次准备直接遗弃掉旧代码,重新笼统好全体文件夹基架。不论事务中到底是不是文件夹都笼统成文件夹,只需文件夹满意父子级联系、并且绑定了一些文件都走同一个逻辑

全面笼统文件夹实体

规范的数据库结构首要便是三张表,FOLDER表存储文件夹信息、FOLDER_REL表存储文件夹与文件相关表、FILE表存储文件信息。

改版文件夹操作第一步就得先界说出笼统文件夹来,不论数据库中的代码结构和事务含义,只需是能够被笼统成文件夹的都能够按一致的文件夹操作。

首要功用便是笼统代码,把每一步都剥离出来,搞一个策略形式,抽出一个AbstractFolderService笼统类界说办法,提供一个FolderBaseServiceImpl以规范的FOLDER表做基础完成。

办法入参封装一个查询基本信息目标,

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public static class FolderKey {
    @ApiModelProperty(value = "文件夹id")
    private String folderId;
    @ApiModelProperty(value = "操作文件夹类型")
    private Integer type;
    @ApiModelProperty(value = "企业域id做数据阻隔")
    private String entId;
}

Abstract层接下来界说各种通用办法,AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo>这个先是后面会提到的文件夹树逻辑,以及假如不是规范FOLDER目标的话只需求界说对应的泛型即可开箱即用

public abstract class AbstractFolderService extends AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
    // 检查文件夹称号是否存在,新建时运用
    public abstract void checkFolderNameExist(FolderBaseForm.FolderKey folderKey, String folderName);
    // 获取文件夹概况,Q为集成了笼统文件夹的泛型
    public abstract Q checkFolderIdAndGetInfo(FolderBaseForm.FolderKey folderKey);
    // 获取文件夹最大序号用于排序
    public abstract Integer getMaxSeqNo(FolderBaseForm.FolderKey folderKey);
    // 保存文件夹
    public abstract String saveFolder(Q folderInfo);
    // 删去文件夹
    public abstract void deleteFolderFromDb(List<FolderBaseForm.FolderKey> folderIdList);
    // 删去文件夹与文件相相联系
    public abstract void deleteFolderFileRelFromDb(FolderDto.FolderRelBatchDel folderRelBatchDel);
    // 获取文件与文件夹相相联系
    public abstract FolderBaseForm.FolderKey getFolderByFile(String fileId, String userId, String entId, Integer treeType,
                                                             String libFolderId);
    // 获取子文件夹
    public abstract List<String> getSubFolderList(FolderBaseForm.FolderKey folderKey, boolean includeSub);
    // 获取指定文件夹下的文件
    public abstract List<String> getFileListFromFolder(FolderBaseForm.FolderKey folderKey, boolean includeSub);
    // 更新文件夹与文件相相联系
    public abstract void updateFolderFileRel(String fileId, FolderBaseForm.FolderKey folderKey, Integer seqNo);
    // 更新文件夹folder-path(便于查询用的folder_id链路)
    public abstract void updateFolderPath(FolderBaseForm.FolderKey folderKey, String oldFolderPath, String newFolderPath);
    // 更新父级文件夹
    public abstract void updateFolderParentIdAndSeqNo(FolderBaseForm.FolderKey folderKey, String parentFolderId, Integer seqNo);
    // 更新文件夹基础信息
    public abstract void updateFolderNameAndComment(FolderBaseForm.FolderKey folderKey, String folderName, String comment);
}

笼统好基础文件夹操作后完成一份规范文件夹完成类去完成笼统出来的基础办法

public class FolderBaseServiceImpl extends AbstractFolderService {
}

如此一来一切后续新加的文件夹操作都能够承继FolderBaseServiceImpl, 把基础的办法都承继过来,假如在事务上有差异的话能够单独重写这个办法,在上层去调用的时分是无感知的。

全面优化加快接口速度

增修改架子搞好之后就该查了,查询可谓是重中之重的功用,特别是构造树的逻辑,回想当时在python代码完成的树逻辑上debug。。。

用户打开数据管理页面第一个接口加载,加载速度直接影响了用户体会,所以这块规划上也需求揣摩一下。能够简略的将树查询拆成三块:

  1. 查询一切指定类型的文件夹

  2. 判别查询是否要带着文件,不需求直接过

    1. 查询一切文件夹下的文件相相联系
    2. 拿到相相联系的file_ids查询一切文件信息
  3. 根据父子相关字段组成树

简略来讲便是还需求一个笼统类去完成基础的拼装树逻辑,这里不只需求完成办法还要用泛型笼统各种事务场景下的文件夹、文件夹相相联系、文件。


    /*
    *笼统文件
    */
    @Data
    @SuperBuilder(toBuilder = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public static class FolderFile {
        private String fileId;
        private String entId;
    }   
    /**
     * 笼统文件夹(folder、topic、category)
     */
    public static class FolderInfo {
        private String folderId;
        private String folderName;
        private String comment;
        private Integer seqNo;
        private String parentId;
        private String entId;
        private String userId;
        private Integer treeType;
        private String folderPath;
        private String creator;
        private String englishName;
    }
    /**
     * 笼统文件夹与工作联系
     */
    @Data
    @SuperBuilder(toBuilder = true)
    public static class FolderFileRel {
        private String folderId;
        private String fileId;
    }

这样就能够运用泛型来一致整个操作,规则运用这套框架只需求承继FolderFile、FolderInfo、FolderFileRel即可。

那么笼统出一致的树结构代码

public abstract class AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
    // 获取一切文件夹
    public abstract List<? extends Q> getFolderInfos(String entId, Integer treeType);
    // 获取一切文件夹相相联系
    public abstract List<? extends P> getFolderFileRel(String entId, Integer treeType);
    // 获取一切文件概况
    public abstract List<? extends T> getFiles(List<String> fileIds, String entId);
}

树结构代码中只处理拿到各类信息后组成树的逻辑,详细如何拿到各类信息调集需求各个完成类去完成,这样的话咱们之前界说的AbstractFolderService笼统基础文件夹操作类就能够承继AbstractTreeService树结构类,这三个办法既能够在FolderBaseServiceImpl得到完成。

如何界说通用树结构的回来类型,不同场景下回来的信息也不同,比方专题场景下没有英文名那么通用VO中回来一个english_name为null也不合适,只能再运用泛型来界说。

    public static class FolderTree<T, E extends FolderInfo> {
        private E folderInfo;
        // 详细内容经过泛型界说确保通用
        private List<FolderTree<T, ? extends FolderInfo>> subFolder;
        private List<T> fileList;
    }

结构分红三部分,本级文件夹概况、子级文件夹列表、本级文件夹相关文件列表。详细的转树逻辑就不贴了,本质上是一个深度优先查找dfs的进程,根据parent_id形成父子级相相联系。

全面建造易扩展接口

整完这俩大块逻辑其实在大部分场景下都适用了,但是咱们这个场景下还不够,因为本次改动首要是后端模块改动,尽量不要防止回来的数据结构发生变化,但是上述的树查询逻辑中就现已把VO都改掉了,还需求一步转成旧VO的逻辑

那就再界说一个旧版VO的基础类,因为旧版的逻辑也并不是全都一致的,那就在原来的基础上再加一个转化外部旧版VO的泛型。

public abstract class AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
    // 获取一切文件夹
    public abstract List<? extends Q> getFolderInfos(String entId, Integer treeType);
    // 获取一切文件夹相相联系
    public abstract List<? extends P> getFolderFileRel(String entId, Integer treeType);
    // 获取一切文件概况
    public abstract List<? extends T> getFiles(List<String> fileIds, String entId);
    // 转化外部VO
    public abstract G convertGlobalTreeVo(FolderTree<T, ? extends FolderInfo> rootTreeNode);
}

转化逻辑还是交由完成类处理,不同的事务场景需求的字段也不一样,只需求界说一个全局笼统旧版外部VO类

public static class GlobalBaseTreeVo {
    private String folderId;
    private String parentId = "";
    @JsonProperty("sub_levels")
    @Builder.Default
    private List<? super GlobalBaseTreeVo> subLevels = new ArrayList<>();
}

转化旧VO逻辑运用广度优先算法bfs完成,这里贴一下吧,自从不做ACM之后好久没有写算法代码了,偶尔逮到一两个场景完成出来还是有些恍如隔世的感觉。

public G bfsConvertVo(FolderTree<T, FolderTreeDto.FolderInfo> innerRootTreeNode) {
        Map<String, G> parentNodeMap = new HashMap<>();
        G resultVo = this.convertGlobalTreeVo(innerRootTreeNode);
        parentNodeMap.put(resultVo.getFolderId(), resultVo);
        LinkedBlockingQueue<FolderTree<T, ? extends FolderInfo>> bfsQueue = new LinkedBlockingQueue<>(innerRootTreeNode.getSubFolder());
        while (!bfsQueue.isEmpty()) {
            FolderTree<T, ? extends FolderInfo> firstObj = bfsQueue.poll();
            G currentNode = this.convertGlobalTreeVo(firstObj);
            G parentNode = parentNodeMap.get(currentNode.getParentId());
            parentNode.getSubLevels().add(currentNode);
            parentNodeMap.put(currentNode.getFolderId(), currentNode);
            bfsQueue.addAll(firstObj.getSubFolder());
        }
        return resultVo;
    }

如此一来这套结构能够满意公司一切类文件夹结构的增修改查逻辑的处理,树查询接口从3s优化到200ms,代码结构清晰,后期在次基础上开发新的文件夹功用也十分简略。

高绩效我拿定了!