三級分類的資料表設計和構造API資料

語言: CN / TW / HK

theme: v-green

攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第4天,點選檢視活動詳情

如此的業務需求應該說是每個專案中的基本吧。

諸如下圖這種:

一級選單下有二級選單和三級選單。與此類似的應用場景還有很多很多,如省市縣的三級聯動、後臺管理選單、部門展示等等。

一、資料表設計

先看看錶結構吧

具體的 sql 檔案,在貼的原始碼倉庫中有。

其中最重要的就是理清父子層級的關係就好了,知道是個什麼樣子設計的,如何去管理他們,那這些就都不是事啦。

二級評論的表的設計方法其實也大致如此。

資料表中對應的資料關係大致如下:

二、專案程式碼

技術就是常用的 SpringBoot、Mybatis-Plus

另外還用了下Mybatis-Plus逆向生成器,還有Java的函數語言程式設計

3.1、匯入依賴

完整依賴如下:

``` org.springframework.boot spring-boot-dependencies 2.5.2 8 8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test com.baomidou mybatis-plus-boot-starter 3.4.1 com.alibaba druid-spring-boot-starter 1.2.6 mysql mysql-connector-java

    <!--進行Spring Boot配置檔案部署時,發出警告Spring Boot Configuration Annotation Processor not configured,但是不影響執行
        它的意思是“Spring Boot配置註解執行器沒有配置”,配置註解執行器的好處是什麼。
        配置註解執行器配置完成後,當執行類中已經定義了物件和該物件的欄位後,在配置檔案中對該類賦值時,便會非常方便的彈出提示資訊。
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!--mybatis-plus逆向工程-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

```

關於 Mybatis-plus 的版本,最新的那個逆向生成器不太熟,就用了舊版本的。感興趣的可以去試一試新版本的。

3.2、逆向生成程式碼

``` import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; import org.junit.platform.commons.util.StringUtils;

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

// 演示例子,執行 main 方法控制檯輸入模組表名回車自動生成對應專案目錄中 public class CodeGenerator {

/**
 * <p>
 * 讀取控制檯內容
 * </p>
 */
public static String scanner(String tip) {
    Scanner scanner = new Scanner(System.in);
    StringBuilder help = new StringBuilder();
    help.append("請輸入" + tip + ":");
    System.out.println(help.toString());
    if (scanner.hasNext()) {
        String ipt = scanner.next();
        if (StringUtils.isNotBlank(ipt)) {
            return ipt;
        }
    }
    throw new MybatisPlusException("請輸入正確的" + tip + "!");
}

public static void main(String[] args) {
    // 程式碼生成器
    AutoGenerator mpg = new AutoGenerator();

    // 全域性配置
    GlobalConfig gc = new GlobalConfig();
    final String projectPath = System.getProperty("user.dir");
    gc.setOutputDir(projectPath + "/src/main/java");
    gc.setAuthor("nzc");
    gc.setOpen(false);
    // gc.setSwagger2(true); 實體屬性 Swagger2 註解
    mpg.setGlobalConfig(gc);

    // 資料來源配置
    DataSourceConfig dsc = new DataSourceConfig();
    dsc.setUrl("jdbc:mysql://localhost:3306/task1?useUnicode=true&useSSL=false&characterEncoding=utf8");
    // dsc.setSchemaName("public");
    dsc.setDriverName("com.mysql.cj.jdbc.Driver");
    dsc.setUsername("root");
    dsc.setPassword("123456");
    mpg.setDataSource(dsc);

    // 包配置
    final PackageConfig pc = new PackageConfig();
    pc.setModuleName(scanner("模組名"));
    pc.setParent("com.nzc");
    mpg.setPackageInfo(pc);

    // 自定義配置
    InjectionConfig cfg = new InjectionConfig() {
        @Override
        public void initMap() {
            // to do nothing
        }
    };

    // 如果模板引擎是 freemarker
    //String templatePath = "/templates/mapper.xml.ftl";
    // 如果模板引擎是 velocity
    String templatePath = "/templates/mapper.xml.vm";

    // 自定義輸出配置
    List<FileOutConfig> focList = new ArrayList<>();
    // 自定義配置會被優先輸出
    focList.add(new FileOutConfig(templatePath) {
        @Override
        public String outputFile(TableInfo tableInfo) {
            // 自定義輸出檔名 , 如果你 Entity 設定了前後綴、此處注意 xml 的名稱會跟著發生變化!!
            return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                    + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
        }
    });
    cfg.setFileOutConfigList(focList);
    mpg.setCfg(cfg);

    // 配置模板
    TemplateConfig templateConfig = new TemplateConfig();

    templateConfig.setXml(null);
    mpg.setTemplate(templateConfig);

    // 策略配置
    StrategyConfig strategy = new StrategyConfig();
    strategy.setNaming(NamingStrategy.underline_to_camel);

    strategy.setColumnNaming(NamingStrategy.underline_to_camel);
    strategy.setEntityLombokModel(true);
    strategy.setRestControllerStyle(true);
    // 公共父類
    // 寫於父類中的公共欄位
    strategy.setSuperEntityColumns("id");
    strategy.setInclude(scanner("表名,多個英文逗號分割").split(","));
    strategy.setControllerMappingHyphenStyle(true);
    strategy.setTablePrefix("pms_");
    mpg.setStrategy(strategy);
    mpg.setTemplateEngine(new VelocityTemplateEngine());
    mpg.execute();
}

} ```

執行程式碼:

執行結果

預設裡面是沒有方法的話,不過用來寫個基本的小demo是非常方便的。

3.3、業務程式碼

controller、mapper、entity都不是重點,一筆帶過了哈。

``` @RestController @RequestMapping("/category") public class CategoryController {

@Autowired
ICategoryService categoryService;
/**
 * 查詢出所有的資料,以樹形結構組裝起來
 * @return
 */
@GetMapping("/tree")
public Result listTree() {
    List<Category> entities= categoryService.listWithTree();
    return ResultUtil.success(entities);
}

} ```

``` public interface CategoryMapper extends BaseMapper {

} ```

``` @Data @EqualsAndHashCode(callSuper = false) @TableName("pms_category") public class Category implements Serializable {

private static final long serialVersionUID = 1L;

@TableId(value = "cat_id", type = IdType.AUTO)
private Long catId;

private String name;

private Long parentCid;

private Integer catLevel;

private Integer showStatus;

private Integer sort;

private String icon;

private String productUnit;

private Integer productCount;

@TableField(exist = false)
private List<Category> categoryChild;

} ```

``` public interface ICategoryService extends IService {

/**
 * 封裝成三級選單形式
 * @return
 */
List<Category> listWithTree();

} ```

真正的業務是在 serviceImpl 中

``` @Service public class CategoryServiceImpl extends ServiceImpl implements ICategoryService {

// @Autowired // CategoryMapper categoryMapper;

@Override
public List<Category> listWithTree() {
    //1、查詢出所有的分類
    List<Category> allCategory = baseMapper.selectList(null);
    //2、組裝成父子的樹形結構
    //2.1、找到所有的一級分類
    List<Category> categoriesLevel1 = allCategory.stream()
          // filter 方法就如名稱一樣,就是用來過濾的 
          // 滿足條件的會留下,到最後會返回一個流式物件
            .filter(category ->
                    category.getParentCid() == 0
            )
          // map 的理解百話點說就是可以對傳入的物件做出修改 
          // 這裡的意思就是找出當前一級分類中的二級分類,然後再set進去
            // 最後再返回一個流式物件
            .map((menu) -> {
                menu.setCategoryChild(getChildrens(menu, allCategory));
                return menu;
            })
          // 見名知意,這就是排序 ,其實也算是重定義Java比較器
           .sorted((menu1, menu2) -> {
                return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
            })
          // 就是將流轉換為帶泛型的List物件
            .collect(Collectors.toList());
    return categoriesLevel1;
}


private List<Category> getChildrens(Category root, List<Category> all) {
    // 這裡的邏輯和上面一段是一樣的,這裡是構造三級、四級等等多層也是一樣構造的
    List<Category> collect = all.stream()
            .filter(category -> {
                return category.getParentCid().equals(root.getCatId());
            }).map((menu) -> {
                menu.setCategoryChild(getChildrens(menu, all));
                return menu;
            })
            .sorted((menu1, menu2) -> {
                return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
            })
            .collect(Collectors.toList());
    return collect;
}

} ```

還有一些公共程式碼和配置檔案什麼的,在倉庫中都是有的,這裡不再重複貼出來啦。

3.4、測試結果

我就是使用 Postman 測試的,沒有專門畫前端,前端太弱雞啦,求大佬們放過...

原始碼:githubgitee

三、自言自語

真正到了工作的時候,你會發現你寫程式碼和平常還是會有很大差別的,最主要就是體現在業務場景這個方面。

寫 Demo 的時候,主要就想著能寫出個 Demo 就不錯了,但實際上真到了該上業務的時候,綜合起來還是有很多值得思考的問題。

在學習的時候針對一些必要的技術還是可以往深裡挖掘挖掘的,並且自己也要學會根據現學的東西想到應用場景,思考如何整合專案,那些地方還有不足的,專案的擴充套件性如何等等,這些可以不用實現,但是在學習的時候能夠多一些思考,這對於學習一個新技術是很有必要的。