Mybatis和Mybatis Plus代码生成器详解

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
全局流量管理 GTM,标准版 1个月
简介: Mybatis和Mybatis Plus代码生成器详解

前言

背景


在 SpringBoot 项目开发前,关于初始代码的生成,是值得考虑的一件事。当我们根据业务需求完成表设计后,接下来就需要根据表生成相关代码,在 SpringBoot 项目中需要以下几部分内容:


  • entity, 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法 ;


  • mapper,对数据库进行数据持久化操作,它的方法语句是直接针对数据库操作的,主要实现一些增删改查操作,在 mybatis 中方法主要与与 xxx.xml 内相互一一映射;


  • service,业务 service 层,给 controller 层的类提供接口进行调用。一般就是自己写的方法封装起来,就是声明一下,具体实现在 serviceImpl 中;


  • controller,控制层,负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。因为 service 中的方法是我们使用到的,controller 通过接收前端 H5 或者 App 传过来的参数进行业务操作,再将处理结果返回到前端。


除了上述项目架构中最基本的文件,为了更好的管理项目,我们还增加以下几个层级:


  • dto文件,用来分担实体类的功效,可以将查询条件单独封装一个类,以及前后端交互的实体类(有时候我们可能会传入 entity 实体类中不存在的字段);


  • vo文件,后台返回给前台的数据结构,同样可以自定义字段;


  • struct文件,用来处理 dto 、entity、vo 文件之间的转换。


项目中使用的 ORM 框架多为 Mybatis 和 Mybatis Plus,虽然各自的官方文档都有代码生成器配置,但是过于简单,无法满足实际需求,因此整理出一套通用的代码生成器,势在必行。


在开始本文之前,首先介绍一下要用到的知识点。


知识点

FreeMarker


FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像 PHP 那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。


Mybatis


MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。


Mybatis Plus


MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

本文主要讲述选择使用 Mybatis 和 Mybatis Plus 时,相关代码文件的生成过程。


JCommander


JCommander 是一个用于解析命令行参数的Java框架,支持解析所有基本的数据类型,也支持将命令行解析成用户自定义的类型,只需要写一个转变函数。

接下来就进行代码实战环节。


实战

首先新建一个 maven 项目,命名为 mybatis-generator。


基本环境配置


导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.msdn.generator</groupId>
    <artifactId>mybatis-generator</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
        <logback.version>1.2.3</logback.version>
        <slf4j.version>1.7.30</slf4j.version>
        <fastjson.version>1.2.73</fastjson.version>
        <hutool.version>5.5.8</hutool.version>
        <mysql.version>8.0.19</mysql.version>
        <swagger.version>1.9.1.RELEASE</swagger.version>
        <mybatis.version>2.1.4</mybatis.version>
        <mapper.version>4.1.5</mapper.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>${mapper.version}</version>
        </dependency>
        <!--JCommander解析命令行参数-->
        <dependency>
            <groupId>com.beust</groupId>
            <artifactId>jcommander</artifactId>
            <version>1.78</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
            <version>2.4.6</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
复制代码


配置文件


application.yml 文件内容如下:


server:
  port: 8525
#并无实际意义,实际项目中具体配置
spring:
  datasource:
    username: root
    password: xxxx
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useSSL=true&serverTimezone=UTC&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
复制代码


代码生成

实体类


为了接收相关配置参数,我们通过 JCommander 解析命令行参数,此处创建对应的实体类 GenerateParameter来接收这些参数。

@Getter
@Setter
@ApiModel("使用帮助")
@Parameters(commandDescription = "使用帮助")
public class GenerateParameter {
    @ApiModelProperty("mysql主机名")
    @Parameter(names = {"--host", "-h"}, description = "mysql主机名")
    private String host;
    @ApiModelProperty("mysql端口")
    @Parameter(names = {"--port", "-P"}, description = "mysql端口")
    private Integer port;
    @ApiModelProperty("mysql用户名")
    @Parameter(names = {"--username", "-u"}, description = "mysql用户名")
    private String username;
    @ApiModelProperty("mysql密码")
    @Parameter(names = {"--password", "-p"}, description = "mysql密码")
    private String password;
    @ApiModelProperty("mysql数据库名")
    @Parameter(names = {"--database", "-d"}, description = "mysql数据库名")
    private String database;
    @ApiModelProperty("mysql数据库表")
    @Parameter(names = {"--table", "-t"}, description = "mysql数据库表")
    private List<String> table;
    @ApiModelProperty("业务模块名")
    @Parameter(names = {"--module", "-m"}, description = "业务模块名")
    private String module;
    @ApiModelProperty("业务分组,目前是base和business")
    @Parameter(names = {"--group", "-g"}, description = "业务分组,目前是base和business")
    private String group;
    @ApiModelProperty("是否按表名分隔目录")
    @Parameter(names = {"--flat"}, description = "是否按表名分隔目录")
    private boolean flat;
    @ApiModelProperty("orm框架选择")
    @Parameter(names = {"--type"}, description = "orm框架选择")
    private String type;
    @ApiModelProperty("查看帮助")
    @Parameter(names = "--help", help = true, description = "查看帮助")
    private boolean help;
    @ApiModelProperty("表名截取起始索引,比如表名叫做t_sale_contract_detail,生成的实体类为ContractDetail,则该字段为7")
    @Parameter(names = {"--tableStartIndex", "-tsi"}, description = "表名截取起始索引")
    private String tableStartIndex;
}
复制代码


当连接上数据库后,我们需要解析读取的表结构,包括获取表字段,字段备注,字段类型等内容,对应此处创建的  Column 类。


/**
 * 数据表的解析内容
 */
@Data
public class Column {
    /**
     * 是否是主键
     */
    private Boolean isPrimaryKey;
    /**
     * 主键类型,Mybatis Plus实体类需要使用,默认为ASSIGN_ID(3)
     */
    private String primaryKeyType = "ASSIGN_ID";
    /**
     * 数据库表名称
     */
    private String tableName;
    /**
     * 表描述
     */
    private String tableDesc;
    /**
     * 数据库字段名称
     **/
    private String fieldName;
    /**
     * 数据库字段类型
     **/
    private String fieldType;
    /**
     * Java类型
     */
    private String javaType;
    /**
     * 是否是数字类型
     */
    private Boolean isNumber;
    /**
     * 数据库字段驼峰命名,saleBooke
     **/
    private String camelName;
    /**
     * 数据库字段Pascal命名,SaleBook
     **/
    private String pascalName;
    /**
     * 数据库字段注释
     **/
    private String comment;
    private String field;
    private String key;
    private Boolean isConfig;
}
复制代码


最后创建一个常量类 Config,来存储常量信息。


public class Config {
    public static final String OutputPath = "." + File.separator + "output";
    public static final String Author = "hresh";
}
复制代码


服务类


首先定义 FreeMarker 的使用代码:


/**
 * 使用FreeMarker 根据设定好的文件模板来生成相关文件
 */
@Service
public class FreemarkerService {
    private static final Logger logger = LoggerFactory.getLogger(FreemarkerService.class);
    @Autowired
    private Configuration configuration;
    /**
     * 输出文件模板
     *
     * @param templateName      resources 文件夹下的模板名,比如说model.ftl,是生成实体类的模块
     * @param dataModel         表名,字段名等内容集合
     * @param filePath          输出文件名,包括路径
     * @param generateParameter
     * @throws Exception
     */
    public void write(String templateName, Map<String, Object> dataModel, String filePath, GenerateParameter generateParameter) throws Exception {
        // FTL(freemarker templete language)模板的文件名称
        Template template = configuration.getTemplate(dataModel.get("type") + File.separator + templateName + ".ftl");
        File file;
        // 判断是不是多表,如果是,则按照表名生成各自的文件夹目录
        if (generateParameter.isFlat()) {
            file = new File(Config.OutputPath + File.separator + dataModel.get("tempId") + File.separator + filePath);
        } else {
            file = new File(Config.OutputPath + File.separator + dataModel.get("tempId") + File.separator + dataModel.get("tableName") + File.separator + filePath);
        }
        if (!file.exists()) {
            file.getParentFile().mkdirs();
            file.createNewFile();
        }
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
        template.process(dataModel, outputStreamWriter);
        fileOutputStream.flush();
        fileOutputStream.close();
    }
}
复制代码


接下来是本项目最核心的代码,通过读取数据表,获取表的定义信息,然后利用 FreeMarker 读取 Ftl 模板文件来生成关于该表的基础代码。


基础服务类 BaseService


public class BaseService {
    public String getUrl(GenerateParameter generateParameter) {
        return "jdbc:mysql://" + generateParameter.getHost() + ":" + generateParameter.getPort() + "/" + generateParameter.getDatabase() + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8";
    }
    // 数据库连接,类似于:DriverManager.getConnection("jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC","root","password");
    public Connection getConnection(GenerateParameter generateParameter) throws Exception {
        return DriverManager.getConnection(getUrl(generateParameter), generateParameter.getUsername(), generateParameter.getPassword());
    }
    /**
     * 根据表具体位置,获取表中字段的具体信息,包括字段名,字段类型,备注等
     *
     * @param tableName
     * @param parameter
     * @return
     * @throws Exception
     */
    public List<Column> getColumns(String tableName, GenerateParameter parameter, String[] commonColumns) throws Exception {
        // 数据库连接
        Connection connection = getConnection(parameter);
        // 获取表定义的字段信息
        ResultSet resultSet = connection.createStatement().executeQuery("SHOW FULL COLUMNS FROM " + tableName);
        List<Column> columnList = new ArrayList<>();
        while (resultSet.next()) {
            String fieldName = resultSet.getString("Field");
            Column column = new Column();
            // 判断是否是主键
            column.setIsPrimaryKey("PRI".equals(resultSet.getString("Key")));
            // 获取字段名称
            column.setFieldName(fieldName);
            // Mybatis Plus特定字段从核心类里获取
            if (Objects.nonNull(commonColumns) && Arrays.asList(commonColumns).contains(fieldName)) {
                column.setIsCommonField(true);
            } else {
                column.setIsCommonField(false);
            }
            // 获取字段类型
            column.setFieldType(resultSet.getString("Type").replaceAll("\\(.*\\)", ""));
            switch (column.getFieldType()) {
                case "json":
                case "longtext":
                case "char":
                case "varchar":
                case "text":
                    column.setJavaType("String");
                    column.setIsNumber(false);
                    break;
                case "date":
                case "datetime":
                    column.setJavaType("Date");
                    column.setIsNumber(false);
                    break;
                case "bit":
                    column.setJavaType("Boolean");
                    column.setIsNumber(false);
                    break;
                case "int":
                case "tinyint":
                    column.setJavaType("Integer");
                    column.setIsNumber(true);
                    break;
                case "bigint":
                    column.setJavaType("Long");
                    column.setIsNumber(true);
                    break;
                case "decimal":
                    column.setJavaType("BigDecimal");
                    column.setIsNumber(true);
                    break;
                case "varbinary":
                    column.setJavaType("byte[]");
                    column.setIsNumber(false);
                    break;
                default:
                    throw new Exception(tableName + " " + column.getFieldName() + " " + column.getFieldType() + "类型没有解析");
            }
            // 转换字段名称,receipt_sign_name字段改为 receiptSignName
            column.setCamelName(StringUtils.underscoreToCamel(column.getFieldName()));
            // 首字母大写
            column.setPascalName(StringUtils.capitalize(column.getCamelName()));
            // 字段在数据库的注释
            column.setComment(resultSet.getString("Comment"));
            columnList.add(column);
        }
        return columnList;
    }
    /**
     * 获取表的描述
     *
     * @param tableName
     * @param parameter
     * @return
     * @throws Exception
     */
    public String getTableComment(String tableName, GenerateParameter parameter) throws Exception {
        Connection connection = getConnection(parameter);
        ResultSet resultSet = connection.createStatement().executeQuery("SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '" + parameter.getDatabase()
                + "' AND table_name = '" + tableName + "'");
        String tableComment = "";
        while (resultSet.next()) {
            tableComment = resultSet.getString("table_comment");
        }
        return tableComment;
    }
}
复制代码


GenerateService 获取表信息生成相关代码


@Service
public class GenerateService extends BaseService{
    private static final Logger logger = LoggerFactory.getLogger(GenerateService.class);
    @Autowired
    private FreemarkerService freemarkerService;
    /**
     * @param tableName 数据库表名
     * @param parameter 模块名
     * @param uuid
     * @throws Exception
     */
    public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception {
        // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
        String packagePrefix = "com.msdn." + parameter.getModule();
        // 分组
        if (!StringUtils.isEmpty(parameter.getGroup())) {
            packagePrefix = packagePrefix + "." + parameter.getGroup();
        }
        // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
        // 现在表名截取起始索引该由参数配置
//        int index = tableName.indexOf("_", 2);
        Integer index = new Integer(parameter.getTableStartIndex());
        // 驼峰命名,首字母小写,比如:contractDetail
        String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
        Map<String, Object> dataModel = new HashMap<>();
        //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
        List<Column> columns = getColumns(tableName, parameter,Config.COMMON_COLUMNS);
        Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null);
        dataModel.put("package", packagePrefix);
        dataModel.put("camelName", camelName);
        // 首字母转大写,作为实体类名称等
        dataModel.put("pascalName", StringUtils.capitalize(camelName));
        dataModel.put("moduleName", parameter.getModule());
        dataModel.put("tableName", tableName);
        // 表描述
        dataModel.put("tableComment", getTableComment(tableName, parameter));
        dataModel.put("columns", columns);
        dataModel.put("primaryColumn", primaryColumn);
        dataModel.put("tempId", uuid);
        dataModel.put("author", Config.Author);
        dataModel.put("date", DateUtil.now());
        dataModel.put("type", parameter.getType());
        logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
        // 生成模板代码
        logger.info("**********开始生成Model模板文件**********");
        generateModel(dataModel, parameter);
        logger.info("**********开始生成VO视图模板文件**********");
        generateVO(dataModel, parameter);
        logger.info("**********开始生成DTO模板文件**********");
        generateDTO(dataModel, parameter);
//        logger.info("**********开始生成Struct模板文件**********");
//        generateStruct(dataModel, parameter);
        logger.info("**********开始生成Mapper模板文件**********");
        generateMapper(dataModel, parameter);
        logger.info("**********开始生成Service模板文件**********");
        generateService(dataModel, parameter);
        logger.info("**********开始生成Controller模板文件**********");
        generateController(dataModel, parameter);
    }
    /**
     * 生成 controller 模板代码
     *
     * @param dataModel
     * @param generateParameter
     * @throws Exception
     */
    private void generateController(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "controller" + File.separator + dataModel.get("pascalName") + "Controller.java";
        freemarkerService.write("controller", dataModel, path, generateParameter);
    }
    private void generateDTO(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "dto" + File.separator + dataModel.get("pascalName");
        freemarkerService.write("dto", dataModel, path + "DTO.java", generateParameter);
        freemarkerService.write("dto-page", dataModel, path + "QueryPageDTO.java", generateParameter);
    }
    //
    private void generateModel(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "model" + File.separator + dataModel.get("pascalName") + ".java";
        freemarkerService.write("model", dataModel, path, generateParameter);
    }
    private void generateStruct(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "struct" + File.separator + dataModel.get("pascalName") + "Struct.java";
        freemarkerService.write("struct", dataModel, path, generateParameter);
    }
    private void generateMapper(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "mapper" + File.separator + dataModel.get("pascalName") + "Mapper.java";
        freemarkerService.write("mapper", dataModel, path, generateParameter);
        path = "resources" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
        freemarkerService.write("mapper-xml", dataModel, path, generateParameter);
    }
    private void generateService(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "service" + File.separator + dataModel.get("pascalName") + "Service.java";
        freemarkerService.write("service", dataModel, path, generateParameter);
        path = "java" + File.separator + "service" + File.separator + "impl" + File.separator + dataModel.get("pascalName") + "ServiceImpl.java";
        freemarkerService.write("service-impl", dataModel, path, generateParameter);
    }
    private void generateVO(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "java" + File.separator + "vo" + File.separator + dataModel.get("pascalName") + "VO.java";
        freemarkerService.write("vo", dataModel, path, generateParameter);
    }
}
复制代码


控制器


为了更加方便地使用代码生成器,我们通过 swagger 来调用 Rest 服务接口。


@RestController
public class GeneratorController {
    private static final Logger logger = LoggerFactory.getLogger(GeneratorController.class);
    @Autowired
    private GenerateService generateService;
    /*
        // 请求参数
        {
            "database": "db_tl_sale",
            "flat": true,
            "type": "mybatis",
            "group": "base",
            "host": "127.0.0.1",
            "module": "sale",
            "password": "123456",
            "port": 3306,
            "table": [
                "t_xs_sale_contract"
            ],
            "username": "root",
            "tableStartIndex":"5"
        }
     */
    @PostMapping("/generator/build")
    public void build(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception {
        logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
        logger.info("************************************************************");
        String uuid = UUID.randomUUID().toString();
        for (String table : parameter.getTable()) {
            generateService.generate(table, parameter, uuid);
        }
        logger.info("**********模板文件生成完毕,准备下载**********");
        String path = Config.OutputPath + File.separator + uuid;
        //设置响应头控制浏览器的行为,这里我们下载zip
        response.setHeader("Content-disposition", "attachment; filename=code.zip");
        response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
        // 将response中的输出流中的文件压缩成zip形式
        ZipDirectory(path, response.getOutputStream());
        // 递归删除目录
        FileSystemUtils.deleteRecursively(new File(path));
        logger.info("************************************************************");
        logger.info("**********模板文件下载完毕,谢谢使用**********");
    }
    /**
     * 一次性压缩多个文件,文件存放至一个文件夹中
     */
    public static void ZipDirectory(String directoryPath, ServletOutputStream outputStream) {
        InputStream input = null;
        ZipOutputStream output = null;
        try {
            output = new ZipOutputStream(outputStream);
            List<File> files = getFiles(new File(directoryPath));
            for (File file : files) {
                input = new FileInputStream(file);
                output.putNextEntry(new ZipEntry(file.getPath().substring(directoryPath.length() + 1)));
                int temp = 0;
                while ((temp = input.read()) != -1) {
                    output.write(temp);
                }
                input.close();
            }
            output.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static List<File> getFiles(File file) {
        List<File> files = new ArrayList<>();
        for (File subFile : file.listFiles()) {
            if (subFile.isDirectory()) {
                List<File> subFiles = getFiles(subFile);
                files.addAll(subFiles);
            } else {
                files.add(subFile);
            }
        }
        return files;
    }
}
复制代码


启动类


@EnableSwagger2Doc
@SpringBootApplication
public class GeneratorApplication {
    /**
     * 测试的时候添加参数 -h 127.0.0.1 -P 3306 -d db_tl_sale -u root -p 123456 -m sale -g base -t t_xs_sale_contract,t_xs_sale_contract_detail
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) {
        SpringApplication.run(GeneratorApplication.class, args);
    }
}
复制代码


模板文件


定义的模板文件如下图所示:


1.jpg


其它代码


除了上述代码,还有一些工具类,以及公共组件,这里就不一一介绍了,感兴趣的同学可以去 github 上参看相关代码。


效果


启动项目后,直接访问 http://localhost:8525/swagger-ui.html#/。

2.jpg


传入参数根据个人需要按照如下格式整理信息:


{
    "database": "db_tl_sale",
    "flat": true,
    "type": "mybatis",
    "group": "base",
    "host": "127.0.0.1",
    "module": "sale",
    "password": "123456",
    "port": 3306,
    "table": [
        "t_xs_sale_contract"
    ],
    "username": "root",
    "tableStartIndex":"5"
}
复制代码


然后点击执行,执行成功后点击下载,将生成好的代码下载到本地。文件结构如下图所示:


3.jpg


这里截取一部分代码图片,首先是实体类:


4.jpg


然后是查询实体类:


5.jpg


接着是 Service 接口:


6.jpg


以及对应的实现类:


7.jpg


最后是 controller:


8.jpg


扩展

一对多关联查询


resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。


需求:


目前订单类详情查询返回的结果中,除了包含订单类的全部信息,还需要返回多个订单子项的数据,也就是我们常说的一对多关系,那么在实际开发中如何操作呢?


首先我们看一下代码案例:


1、订单类


public class OmsOrder implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "订单id")
    private Long id;
    private Long memberId;
    private Long couponId;
    @ApiModelProperty(value = "订单编号")
    private String orderSn;
    @ApiModelProperty(value = "提交时间")
    private Date createTime;
    @ApiModelProperty(value = "用户帐号")
    private String memberUsername;
    @ApiModelProperty(value = "订单总金额")
    private BigDecimal totalAmount;
    @ApiModelProperty(value = "应付金额(实际支付金额)")
    private BigDecimal payAmount;
    @ApiModelProperty(value = "运费金额")
    private BigDecimal freightAmount;
    @ApiModelProperty(value = "促销优化金额(促销价、满减、阶梯价)")
    private BigDecimal promotionAmount;
    @ApiModelProperty(value = "积分抵扣金额")
    private BigDecimal integrationAmount;
    @ApiModelProperty(value = "优惠券抵扣金额")
    private BigDecimal couponAmount;
    @ApiModelProperty(value = "管理员后台调整订单使用的折扣金额")
    private BigDecimal discountAmount;
    @ApiModelProperty(value = "支付方式:0->未支付;1->支付宝;2->微信")
    private Integer payType;
    @ApiModelProperty(value = "订单来源:0->PC订单;1->app订单")
    private Integer sourceType;
    @ApiModelProperty(value = "订单状态:0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单")
    private Integer status;
    @ApiModelProperty(value = "订单类型:0->正常订单;1->秒杀订单")
    private Integer orderType;
    @ApiModelProperty(value = "物流公司(配送方式)")
    private String deliveryCompany;
    @ApiModelProperty(value = "物流单号")
    private String deliverySn;
    @ApiModelProperty(value = "自动确认时间(天)")
    private Integer autoConfirmDay;
    @ApiModelProperty(value = "可以获得的积分")
    private Integer integration;
    @ApiModelProperty(value = "可以活动的成长值")
    private Integer growth;
    @ApiModelProperty(value = "活动信息")
    private String promotionInfo;
    @ApiModelProperty(value = "发票类型:0->不开发票;1->电子发票;2->纸质发票")
    private Integer billType;
    @ApiModelProperty(value = "发票抬头")
    private String billHeader;
    @ApiModelProperty(value = "发票内容")
    private String billContent;
    @ApiModelProperty(value = "收票人电话")
    private String billReceiverPhone;
    @ApiModelProperty(value = "收票人邮箱")
    private String billReceiverEmail;
    @ApiModelProperty(value = "收货人姓名")
    private String receiverName;
    @ApiModelProperty(value = "收货人电话")
    private String receiverPhone;
    @ApiModelProperty(value = "收货人邮编")
    private String receiverPostCode;
    @ApiModelProperty(value = "省份/直辖市")
    private String receiverProvince;
    @ApiModelProperty(value = "城市")
    private String receiverCity;
    @ApiModelProperty(value = "区")
    private String receiverRegion;
    @ApiModelProperty(value = "详细地址")
    private String receiverDetailAddress;
    @ApiModelProperty(value = "订单备注")
    private String note;
    @ApiModelProperty(value = "确认收货状态:0->未确认;1->已确认")
    private Integer confirmStatus;
    @ApiModelProperty(value = "删除状态:0->未删除;1->已删除")
    private Integer deleteStatus;
    @ApiModelProperty(value = "下单时使用的积分")
    private Integer useIntegration;
    @ApiModelProperty(value = "支付时间")
    private Date paymentTime;
    @ApiModelProperty(value = "发货时间")
    private Date deliveryTime;
    @ApiModelProperty(value = "确认收货时间")
    private Date receiveTime;
    @ApiModelProperty(value = "评价时间")
    private Date commentTime;
    @ApiModelProperty(value = "修改时间")
    private Date modifyTime;
}
复制代码


2、订单子项类


public class OmsOrderItem implements Serializable {
  private static final long serialVersionUID = 1L;
    private Long id;
    @ApiModelProperty(value = "订单id")
    private Long orderId;
    @ApiModelProperty(value = "订单编号")
    private String orderSn;
    private Long productId;
    private String productPic;
    private String productName;
    private String productBrand;
    private String productSn;
    @ApiModelProperty(value = "销售价格")
    private BigDecimal productPrice;
    @ApiModelProperty(value = "购买数量")
    private Integer productQuantity;
    @ApiModelProperty(value = "商品sku编号")
    private Long productSkuId;
    @ApiModelProperty(value = "商品sku条码")
    private String productSkuCode;
    @ApiModelProperty(value = "商品分类id")
    private Long productCategoryId;
    @ApiModelProperty(value = "商品促销名称")
    private String promotionName;
    @ApiModelProperty(value = "商品促销分解金额")
    private BigDecimal promotionAmount;
    @ApiModelProperty(value = "优惠券优惠分解金额")
    private BigDecimal couponAmount;
    @ApiModelProperty(value = "积分优惠分解金额")
    private BigDecimal integrationAmount;
    @ApiModelProperty(value = "该商品经过优惠后的分解金额")
    private BigDecimal realAmount;
    private Integer giftIntegration;
    private Integer giftGrowth;
    @ApiModelProperty(value = "商品销售属性:[{'key':'颜色','value':'颜色'},{'key':'容量','value':'4G'}]")
    private String productAttr;
 }
复制代码


3、前端返回类


public class OmsOrderDetail extends OmsOrder {
    @Getter
    @Setter
    @ApiModelProperty("订单商品列表")
    private List<OmsOrderItem> orderItemList;
}
复制代码


4、OmsOrderMapper.xml 文件中自定义 SQL 语句


<resultMap id="orderDetailResultMap" type="com.macro.mall.dto.OmsOrderDetail" extends="com.macro.mall.mapper.OmsOrderMapper.BaseResultMap">
    <collection property="orderItemList" resultMap="com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap" columnPrefix="item_"/>
</resultMap>
<select id="getDetail" resultMap="orderDetailResultMap">
    SELECT o.*,
    oi.id item_id,
    oi.product_id item_product_id,
    oi.product_sn item_product_sn,
    oi.product_pic item_product_pic,
    oi.product_name item_product_name,
    oi.product_brand item_product_brand,
    oi.product_price item_product_price,
    oi.product_quantity item_product_quantity,
    oi.product_attr item_product_attr
    FROM
    oms_order o
    LEFT JOIN oms_order_item oi ON o.id = oi.order_id
    WHERE
    o.id = #{id}
    ORDER BY oi.id ASC DESC
</select>
复制代码


其中 com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap 是引用自 OmsOrderItemMapper.xml 文件中的定义,


<resultMap id="BaseResultMap" type="com.macro.mall.model.OmsOrderItem">
    <id column="id" property="id" />
    <result column="order_id"  property="orderId" />
    <result column="order_sn" property="orderSn" />
    <result column="product_id" jdbcType="BIGINT" property="productId" />
    <result column="product_pic" jdbcType="VARCHAR" property="productPic" />
    <result column="product_name" jdbcType="VARCHAR" property="productName" />
    <result column="product_brand" jdbcType="VARCHAR" property="productBrand" />
    <result column="product_sn" jdbcType="VARCHAR" property="productSn" />
    <result column="product_price" jdbcType="DECIMAL" property="productPrice" />
    <result column="product_quantity" jdbcType="INTEGER" property="productQuantity" />
    <result column="product_sku_id" jdbcType="BIGINT" property="productSkuId" />
    <result column="product_sku_code" jdbcType="VARCHAR" property="productSkuCode" />
    <result column="product_category_id" jdbcType="BIGINT" property="productCategoryId" />
    <result column="promotion_name" jdbcType="VARCHAR" property="promotionName" />
    <result column="promotion_amount" jdbcType="DECIMAL" property="promotionAmount" />
    <result column="coupon_amount" jdbcType="DECIMAL" property="couponAmount" />
    <result column="integration_amount" jdbcType="DECIMAL" property="integrationAmount" />
    <result column="real_amount" jdbcType="DECIMAL" property="realAmount" />
    <result column="gift_integration" jdbcType="INTEGER" property="giftIntegration" />
    <result column="gift_growth" jdbcType="INTEGER" property="giftGrowth" />
    <result column="product_attr" jdbcType="VARCHAR" property="productAttr" />
</resultMap>
复制代码


5、执行效果


9.jpg


这种查询方式相较于先查主表,再根据主表字段关联查询子表信息,减少了 IO 连接查询次数,效率更高一些。


resultMap模板生成


通过上述代码我们可知,实现一对多关联查询的关键在于定义子项数据(多)的 resultMap 定义,既然我们通过代码生成器生成了基本的项目代码,那么是否可以生成 resultMap 呢?说干就干,代码如下:


1、定义模板 ftl 文件


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package}.mapper.${pascalName}Mapper">
    <resultMap id="BaseResultMap" type="${package}.model.${pascalName}">
    <#list columns as column>
    <#if column.isPrimaryKey>
        <id column="${column.fieldName}" property="${column.camelName}" />
    <#else>
        <result column="${column.fieldName}" property="${column.camelName}" />
    </#if>
    </#list>
    </resultMap>
</mapper>
复制代码


2、编写服务类 XmlGenerateService


@Service
public class XmlGenerateService extends BaseService {
    private static final Logger logger = LoggerFactory.getLogger(XmlGenerateService.class);
    @Autowired
    private FreemarkerService freemarkerService;
    /**
     * @param tableName 数据库表名
     * @param parameter 模块名
     * @param uuid
     * @throws Exception
     */
    public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception {
        // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
        String packagePrefix = "com.msdn." + parameter.getModule();
        // 分组
        if (!StringUtils.isEmpty(parameter.getGroup())) {
            packagePrefix = packagePrefix + "." + parameter.getGroup();
        }
        // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
        // 现在表名截取起始索引该由参数配置
//        int index = tableName.indexOf("_", 2);
        Integer index = new Integer(parameter.getTableStartIndex());
        // 驼峰命名,首字母小写,比如:contractDetail
        String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
        Map<String, Object> dataModel = new HashMap<>();
        //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
        List<Column> columns = getColumns(tableName, parameter, null);
        Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null);
        dataModel.put("package", packagePrefix);
        dataModel.put("camelName", camelName);
        // 首字母转大写,作为实体类名称等
        dataModel.put("pascalName", StringUtils.capitalize(camelName));
        dataModel.put("moduleName", parameter.getModule());
        dataModel.put("tableName", tableName);
        // 表描述
        dataModel.put("tableComment", getTableComment(tableName, parameter));
        dataModel.put("columns", columns);
        dataModel.put("primaryColumn", primaryColumn);
        dataModel.put("tempId", uuid);
        dataModel.put("author", Config.Author);
        dataModel.put("date", DateUtil.now());
        dataModel.put("type", parameter.getType());
        logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
        // 生成模板代码
        logger.info("**********开始生成Model模板文件**********");
        generateXML(dataModel, parameter);
    }
    /**
     * 生成 controller 模板代码
     *
     * @param dataModel
     * @param generateParameter
     * @throws Exception
     */
    private void generateXML(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
        String path = "resources" + File.separator + "xml" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
        freemarkerService.write("mybatis-xml", dataModel, path, generateParameter);
    }
}
复制代码


3、服务接口


@PostMapping("/generator/buildXml")
public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception {
    logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
    logger.info("************************************************************");
    String uuid = UUID.randomUUID().toString();
    for (String table : parameter.getTable()) {
        xmlGenerateService.generate(table, parameter, uuid);
    }
    logger.info("**********模板文件生成完毕,准备下载**********");
    String path = Config.OutputPath + File.separator + uuid;
    //设置响应头控制浏览器的行为,这里我们下载zip
    response.setHeader("Content-disposition", "attachment; filename=code.zip");
    response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
    // 将response中的输出流中的文件压缩成zip形式
    ZipDirectory(path, response.getOutputStream());
    // 递归删除目录
    FileSystemUtils.deleteRecursively(new File(path));
    logger.info("************************************************************");
    logger.info("**********模板文件下载完毕,谢谢使用**********");
}
复制代码


4、通过 swagger 调用 api


10.jpg


5、执行结果


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.msdn.mall.mapper.OmsOrderItemMapper">
    <resultMap id="BaseResultMap" type="com.msdn.mall.model.OmsOrderItem">
        <id column="order_item_id" property="orderItemId" />
        <result column="order_id" property="orderId" />
        <result column="order_sn" property="orderSn" />
        <result column="product_id" property="productId" />
        <result column="product_pic" property="productPic" />
        <result column="product_name" property="productName" />
        <result column="product_brand" property="productBrand" />
        <result column="product_sn" property="productSn" />
        <result column="product_price" property="productPrice" />
        <result column="purchase_amount" property="purchaseAmount" />
        <result column="product_sku_id" property="productSkuId" />
        <result column="product_sku_code" property="productSkuCode" />
        <result column="product_category_id" property="productCategoryId" />
        <result column="sp1" property="sp1" />
        <result column="sp2" property="sp2" />
        <result column="sp3" property="sp3" />
        <result column="promotion_name" property="promotionName" />
        <result column="promotion_money" property="promotionMoney" />
        <result column="coupon_money" property="couponMoney" />
        <result column="integration_money" property="integrationMoney" />
        <result column="real_money" property="realMoney" />
        <result column="gift_integration" property="giftIntegration" />
        <result column="gift_growth" property="giftGrowth" />
        <result column="product_attr" property="productAttr" />
        <result column="is_deleted" property="isDeleted" />
        <result column="create_user_code" property="createUserCode" />
        <result column="create_user_name" property="createUserName" />
        <result column="create_date" property="createDate" />
        <result column="update_user_code" property="updateUserCode" />
        <result column="update_user_name" property="updateUserName" />
        <result column="update_date" property="updateDate" />
        <result column="version" property="version" />
    </resultMap>
</mapper>
复制代码


后续


在生产开发中如果还遇到好玩的东西,会不定期追加更新,希望工具越来越强大。


总结


虽然 Mybatis 和 Mybatis Plus 都有相关的代码生成器配置,但是构建器代码不容易整合,外部调用也不方便,最主要的是无法满足实际需求。为了能够一次性生成所有代码,最终选择 SpringBoot 和 FreeMarker 来构建我们专属的代码生成器。


除了可以生成 Java 相关代码,FreeMarker 还可以根据模板文件来生成前端代码,又或者是 Word 文档等,后续更多功能会根据情况逐步补充的。


感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!



目录
相关文章
|
23天前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
2月前
|
Java 数据库连接 API
springBoot:后端解决跨域&Mybatis-Plus&SwaggerUI&代码生成器 (四)
本文介绍了后端解决跨域问题的方法及Mybatis-Plus的配置与使用。首先通过创建`CorsConfig`类并设置相关参数来实现跨域请求处理。接着,详细描述了如何引入Mybatis-Plus插件,包括配置`MybatisPlusConfig`类、定义Mapper接口以及Service层。此外,还展示了如何配置分页查询功能,并引入SwaggerUI进行API文档生成。最后,提供了代码生成器的配置示例,帮助快速生成项目所需的基础代码。
117 1
|
2月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
151 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
XML Java 关系型数据库
springboot 集成 mybatis-plus 代码生成器
本文介绍了如何在Spring Boot项目中集成MyBatis-Plus代码生成器,包括导入相关依赖坐标、配置快速代码生成器以及自定义代码生成器模板的步骤和代码示例,旨在提高开发效率,快速生成Entity、Mapper、Mapper XML、Service、Controller等代码。
springboot 集成 mybatis-plus 代码生成器
|
3月前
|
SQL XML Java
springboot整合mybatis-plus及mybatis-plus分页插件的使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis-Plus及其分页插件,包括依赖引入、配置文件编写、SQL表创建、Mapper层、Service层、Controller层的创建,以及分页插件的使用和数据展示HTML页面的编写。
springboot整合mybatis-plus及mybatis-plus分页插件的使用
|
4月前
|
Java 数据库连接 mybatis
成功解决: Invalid bound statement (not found) 在已经使用mybatis的项目里引入mybatis-plus,结果不能共存的解决
这篇文章讨论了在已使用MyBatis的项目中引入MyBatis-Plus后出现的"Invalid bound statement (not found)"错误,并提供了解决方法,主要是通过修改yml配置文件来解决MyBatis和MyBatis-Plus共存时的冲突问题。
成功解决: Invalid bound statement (not found) 在已经使用mybatis的项目里引入mybatis-plus,结果不能共存的解决
|
5月前
|
Java 数据库连接 测试技术
mybatis plus 获取新增实体的主键
mybatis plus 获取新增实体的主键
159 8
|
5月前
|
Java 数据库连接 数据库
mybatis plus 更新值为null的字段
mybatis plus 更新值为null的字段
62 7
|
5月前
|
Java 数据库连接 Spring
搭建 spring boot + mybatis plus 项目框架并进行调试
搭建 spring boot + mybatis plus 项目框架并进行调试
99 4
|
5月前
|
SQL Java 数据库连接
idea中配置mybatis 映射文件模版及 mybatis plus 自定义sql
idea中配置mybatis 映射文件模版及 mybatis plus 自定义sql
98 3