MyBatis 逆向工程 + 插件定制,一次性带你搞明白

在使用 MyBatis 的过程中,当项目的数据库表很多时,再手动去一一创建对应的 POJO、Mapper Interface、Mapper XML 无疑是一件繁琐的操作,并且这种重复性的工作也没必要花费过多的精力。为此,MyBatis 也提供了逆向工程工具 MyBatis Generator(简称 MBG)。

MyBatis Generator 是一个专为 MyBatis 框架使用者定制的代码生成器,可以根据我们的数据库表生成对应的 POJO、Mapper Interface 以及 Mapper XML,同时生成的 Mapper 还提供了基本单表的 CRUD 功能。

为什么已经有那么多现成的文章还要再写一次呢?因为我发现目前大多数能检索到的文章还是草草介绍,定制化程度并不高,使得很多胖友读完之后还是无法满足自己的需求,包括版本号的对应关系、如何更具实际需求定制插件等等也很少有文章教你如何去确定,大多数人还是主要靠复制张贴。因此,我认为对 MBG 进行一次较为详细的总结还是很有必要的,这样能够大大提升我们的自由掌控能力,从而根据实际业务需求灵活定制。

快速入门

准备数据库

创建用于测试的数据库:

-- 工厂信息表
DROP TABLE IF EXISTS `factory`;
CREATE TABLE `factory`
(
    f_id   int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '工厂ID',
    f_name varchar(20)      NOT NULL COMMENT '工厂名称',
    status tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '状态:1-正常,2-禁用',
    gmt_create datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    gmt_update datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (f_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT = '工厂信息表';

INSERT INTO factory(f_name)
VALUES ('工厂1'),
       ('工厂2'),
       ('工厂3');

-- 工人表
DROP TABLE IF EXISTS `worker`;
CREATE TABLE `worker`
(
    w_id   int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '工人ID',
    w_name varchar(20)      NOT NULL COMMENT '工人名称',
    f_id   int(10) unsigned DEFAULT 1 COMMENT '工厂ID',
    status tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '状态:1-正常,2-禁用',
    gmt_create datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    gmt_update datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (w_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT = '工人表';

INSERT INTO worker(w_name, f_id)
VALUES ('工人1', 1),
       ('工人2', 2),
       ('工人3', 3);

准备项目/模块

创建对应的测试模块:

保持最基本的文件结构即可,后续的相关目录 MBG 会为我们自动生成。

添加依赖

接下来在项目的 pom.xml 文件中添加所需的相关依赖:

<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.7.18</version>
    </parent>

    <groupId>cn.javgo.mybatis</groupId>
    <artifactId>mybatis_generator</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>mybatis_generator</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- MyBatis Spring Boot Starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

        <!-- MyBatis 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <!-- MBG 核心依赖 -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.7</version>
        </dependency>

        <!-- MySQL 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>runtime</scope>
        </dependency>
      
        <!-- Druid 数据库连接池 -->
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>druid-spring-boot-starter</artifactId>
              <version>1.1.17</version>
          </dependency>

        <!-- log -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
      
        <!-- Spring Boot Swagger Starter -->
          <dependency>
              <groupId>io.springfox</groupId>
              <artifactId>springfox-boot-starter</artifactId>
              <version>3.0.0</version>
          </dependency>
    </dependencies>

    <!-- Maven 构建配置 -->
    <build>
        <plugins>
            <!-- Spring Boot Maven 插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- 配置编译版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

确定依赖版本

这里 Spring Boot 我们使用了目前还在维护的 2.7.18 正式发布版:

对于兼容的 mybatis-spring-boot-starter 的版本我们可以在对应的官方文档查阅,由于我们使用的是 JDK 1.8,按照官方文档提示应该选择 2.3.x 版本:

对于具体的版本号则可以通过 github releases 或者 Maven Repository 获取,这里选择 2.3.3 最新的一版即可:

进而就是 mybatis-generator-core 版本的确定,我们同样可以在官方文档 generator 对应页面获取对应信息:

可以看到,这里标注 MyBatis 3.0 以上的版本对于 MyBatis generator 的版本可以使用任何版本,一般选择最新的急即可,但是我们如何确定使用的 MyBatis 版本呢?可以打开 IDEA Maven 页面找到 mybatis-spring-boot-starter 看看内部依赖的 MyBatis 版本即可:

所以我们这回就可以在 Maven Repository 直接使用最新版即可:

最后就是 MyBatis 的分页插件了,这个没有什么限制,直接在 Github pagehelper-spring-boot 使用最新版即可:

自定义 Java 类型解析器

默认的类型解析器可能不满足特定项目的需求。例如,我们可能希望将数据库中的 TINYINT 类型映射为 Java 中的 Integer 类型,而不是 Byte 类型,以便更好地处理数值范围和兼容性。再就是,随着 Java 8 引入了新的日期时间 API,我们可能希望将数据库中的日期和时间类型映射为新的 Java 类型,如 LocalDateLocalTimeLocalDateTime,而不是使用旧的 Date 类型。

通过自定义类型解析器,我们可以根据项目需求灵活地调整数据类型映射,只需要继承 MyBatis Generator 的默认类型解析器 JavaTypeResolverDefaultImpl 即可:

package cn.javgo.mybatis.generator.internal.type;

import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;

import java.sql.Types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
 * 自定义 Java 类型解析器
 */
public class JavaTypeResolverCustomImpl extends JavaTypeResolverDefaultImpl {

    public JavaTypeResolverCustomImpl() {
        super();
        // 将 Types.TINYINT 类型映射为 Integer 类型
        super.typeMap.put(Types.TINYINT, new JdbcTypeInformation("TINYINT", new FullyQualifiedJavaType(Integer.class.getName())));
        // 将 Types.TIMESTAMP 类型映射为 LocalDateTime 类型
        super.typeMap.put(Types.TIMESTAMP, new JdbcTypeInformation("TIMESTAMP", new FullyQualifiedJavaType(LocalDateTime.class.getName())));
        // 将 Types.DATE 类型映射为 LocalDate 类型
        super.typeMap.put(Types.DATE, new JdbcTypeInformation("DATE", new FullyQualifiedJavaType(LocalDate.class.getName())));
        // 将 Types.TIME 类型映射为 LocalTime 类型
        super.typeMap.put(Types.TIME, new JdbcTypeInformation("TIME", new FullyQualifiedJavaType(LocalTime.class.getName())));
    }

}

自定义注 Mapper 接口解插件

在某些项目开发规范中,可能需要在 Mapper 接口或实体类上统一添加某些注解,如 @Repository@Mapper 或自定义的注解,以满足特定的框架要求或标记作用。手动添加这些注解不仅效率低,而且容易遗漏。使用插件自动添加可以确保每个生成的接口都不会遗漏,提高开发效率。

对于插件的定制,直接继承 PluginAdapter 重写对应的方法即可:

package cn.javgo.mybatis.generator.plugins;

import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.TopLevelClass;

import java.util.List;
import java.util.Properties;

/**
 * 自定义注解插件,用于 MyBatis Generator 生成的接口上添加特定的注解。
 */
public class AnnotationPlugin extends PluginAdapter {

    /**
     * 注解类全路径 (如 org.springframework.stereotype.Repository)
     */
    private String annotationClassName;

    /**
     * 注解类名称(如 @Repository)
     */
    private String annotationName;

    /**
     * 验证列表是否有效
     *
     * @param list 待验证的列表
     * @return 如果列表有效返回true,否则返回false
     */
    @Override
    public boolean validate(List<String> list) {
        // 空实现即可
        return true;
    }

    /**
     * 设置属性(为上面的注解全路径和注解名称赋值)
     *
     * @param properties 包含属性键值对的 Properties 对象
     */
    @Override
    public void setProperties(Properties properties) {
        super.setProperties(properties);
        this.annotationClassName = String.valueOf(properties.get("annotationClass"));
        this.annotationName = String.valueOf(properties.get("annotationName"));
    }

    /**
     * 重写父类方法,判断接口是否由客户端生成
     *
     * @param interfaze 接口对象
     * @param introspectedTable 被内省的表对象,即当前表对象
     * @return 返回父类方法的返回值,即接口是否由客户端生成
     */
    @Override
    public boolean clientGenerated(Interface interfaze, IntrospectedTable introspectedTable) {
        // 创建一个完全限定的 Java 类型对象,类型为注解的类名
        FullyQualifiedJavaType importedType = new FullyQualifiedJavaType(annotationClassName);
        // 将该类型添加到接口的导入类型(import)列表中
        interfaze.addImportedType(importedType);
        // 将注解名称添加到接口的注解列表中
        interfaze.addAnnotation(annotationName);
        // 返回 true,表示接口由客户端生成
        return true;
    }

}

自定义插件覆盖 isMergeable 方法

在开发过程中,经常需要重新生成映射文件以反映数据库表结构的变化。如果不覆盖 isMergeable 方法,默认情况下,新生成的文件会尝试与现有文件合并,这可能导致映射文件中出现重复的内容。另外,在团队开发中,映射文件可能由多人维护。将 isMergeable 设置为 false 可以确保每次生成的都是全新的文件,这样更有利于版本控制和避免冲突。

下面的插件用于控制 MyBatis Generator 是否合并生成的 XML 映射文件。在 MyBatis Generator 的默认行为中,如果映射文件已存在,新生成的映射文件会尝试合并到现有文件中,这可能导致一些问题,例如重复的 SQL 映射语句。所以这个插件还是有必要的。

package cn.javgo.mybatis.generator.internal.plugins;

import org.mybatis.generator.api.GeneratedXmlFile;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;

import java.lang.reflect.Field;
import java.util.List;

/**
 * 自定义插件:覆盖 isMergeable 方法
 */
public class OverIsMergeablePlugin extends PluginAdapter {

    /**
     * 验证列表是否有效
     *
     * @param list 待验证的列表
     * @return 如果列表有效返回true,否则返回false
     */
    @Override
    public boolean validate(List<String> list) {
        // 空实现即可
        return true;
    }

    /**
     * {@inheritDoc}
     * 重写父类方法,当生成 SQL Map 文件时调用。
     *
     * @param sqlMap SQL Map 文件对象
     * @param introspectedTable 被内省的表对象,即当前表对象
     * @return 返回父类方法的返回值,表示是否生成 SQL Map 文件
     */
    @Override
    public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
        try {
            // 获取 sqlMap 对象的 isMergeable 属性,该属性表示是否可合并,即是否生成重复的代码
            Field field = sqlMap.getClass().getDeclaredField("isMergeable");
            // 设置 isMergeable 属性可访问
            field.setAccessible(true);
            // 将 isMergeable 属性设置为 false,表示不可合并
            field.setBoolean(sqlMap, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 返回 true,表示生成 SQL Map 文件
        return true;
    }

}

⚠️ 注意:上面这个自定义类其实有更佳简便的方法,因此在后面的 MyBatis Generator 配置文件中我并没有采用,如果你仍旧想使用上面的方式则将下面的后面配置文件的对应部分的 type 替换即可,之所以保留的目的是告诉你一恶搞定制对应功能的入口和方式,便于你后期定制和扩展。

<!-- 覆盖 XML 文件插件:避免生成重复的XML 文件,导致运行时异常 -->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />

自定义分页插件

分页插件用于在 MyBatis Generator 生成的代码中添加分页功能。主要通过修改 Example 类和相应的 XML 映射文件来实现分页查询。分页是数据库查询中常见的需求,特别是在处理大量数据时,分页可以提高查询效率和响应速度。MyBatis Generator 默认生成的代码并不支持分页,因此需要通过插件来扩展这一功能。通过插件自动添加分页代码,可以避免手动修改生成的代码,提高开发效率和代码的可维护性。

package cn.javgo.mybatis.generator.internal.plugins;

import org.mybatis.generator.api.CommentGenerator;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;

import java.util.List;

/**
 * 分页插件,用于在 MyBatis Generator 生成的代码中添加分页功能。
 */
public class PaginationPlugin extends PluginAdapter {

    /**
     * 验证列表是否有效
     *
     * @param list 待验证的列表
     * @return 如果列表有效返回true,否则返回false
     */
    @Override
    public boolean validate(List<String> list) {
        // 空实现即可
        return true;
    }

    /**
     * 重写父类方法,当生成 ModelExample 类时调用
     *
     * @param topLevelClass 顶层类对象
     * @param introspectedTable 被内省的表对象,即当前表对象
     * @return 返回父类方法的返回值,表示是否生成 ModelExample 类
     */
    @Override
    public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
        // 添加 limitStart 字段及其对应的 getter 和 setter 方法
        addLimit(topLevelClass, introspectedTable, "limitStart");
        // 添加 pageSize 字段及其对应的 getter 和 setter 方法
        addLimit(topLevelClass, introspectedTable, "pageSize");
        // 调用父类的 modelExampleClassGenerated 方法,并返回其结果
        return super.modelExampleClassGenerated(topLevelClass, introspectedTable);
    }


    /**
     * 在顶层类和内省表中添加分页查询的 limit 字段以及对应的 getter 和 setter 方法
     *
     * @param topLevelClass 顶层类对象
     * @param introspectedTable 内省表对象
     * @param name limit 字段的名称
     */
    private void addLimit(TopLevelClass topLevelClass, IntrospectedTable introspectedTable, String name) {
        // 获取注释生成器
        CommentGenerator commentGenerator = context.getCommentGenerator();

        // 创建 limit 字段
        Field field = new Field(name, FullyQualifiedJavaType.getIntInstance());
        field.setVisibility(JavaVisibility.PROTECTED);
        field.setType(FullyQualifiedJavaType.getIntInstance());
        field.setName(name);
        field.setInitializationString("-1");

        // 为 limit 字段添加注释
        commentGenerator.addFieldComment(field, introspectedTable);

        // 将 limit 字段添加到顶层类中
        topLevelClass.addField(field);

        // 获取 limit 字段的首字母,并转换为大写
        char c = name.charAt(0);
        String camel = Character.toUpperCase(c) + name.substring(1);

        // 创建 setter 方法
        Method method = new Method("set" + camel);
        method.setVisibility(JavaVisibility.PUBLIC);
        method.setName("set" + camel);
        method.addParameter(new Parameter(FullyQualifiedJavaType.getIntInstance(), name));
        method.addBodyLine("this." + name + "=" + name + ";");

        // 为 setter 方法添加注释
        commentGenerator.addGeneralMethodComment(method, introspectedTable);

        // 将 setter 方法添加到顶层类中
        topLevelClass.addMethod(method);

        // 创建 getter 方法
        method = new Method("get" + camel);
        method.setVisibility(JavaVisibility.PUBLIC);
        method.setReturnType(FullyQualifiedJavaType.getIntInstance());
        method.setName("get" + camel);
        method.addBodyLine("return " + name + ";");

        // 为 getter 方法添加注释
        commentGenerator.addGeneralMethodComment(method, introspectedTable);

        // 将 getter 方法添加到顶层类中
        topLevelClass.addMethod(method);
    }


    /**
     * 重写父类方法,当生成 sqlMap 中不包含 BLOBs 的 selectByExample 元素时调用
     *
     * @param element Xml 元素对象
     * @param introspectedTable 被内省的表对象,即当前表对象
     * @return 返回父类方法的返回值,表示是否生成 sqlMap 中不包含 BLOBs 的 selectByExample 元素
     */
    @Override
    public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
        // 创建一个新的 XmlElement 对象,用于表示 if 元素
        XmlElement ifXmlElement = new XmlElement("if");
        // 为 if 元素添加属性 test,值为 "limitStart >= 0"
        ifXmlElement.addAttribute(new Attribute("test", "limitStart >= 0"));
        // 在 if 元素中添加文本元素,内容为 "limit ${limitStart} , ${pageSize}"
        ifXmlElement.addElement(new TextElement("limit ${limitStart} , ${pageSize}"));

        // 将 if 元素添加到传入的 element 中
        element.getElements().add(ifXmlElement);
        // 调用父类的 sqlMapSelectByExampleWithoutBLOBsElementGenerated 方法,并返回其结果
        return super.sqlMapSelectByExampleWithoutBLOBsElementGenerated(element, introspectedTable);
    }

}

自定义 JavaBean 注解插件

有时候为了更方便的集成 API 文档我们都会采用诸如 Swagger 的注解对 POJO 的属性进行标识,为了实现这一目的,我们可以通过自定义 DefaultCommentGenerator 来实现。在下面的类中,我还添加了基本的类注释,包括作者信息、创建时间等 JavaDoc 内容。

对应代码如下:

package cn.javgo.mybatis.generator.internal;

import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.internal.DefaultCommentGenerator;
import org.mybatis.generator.internal.util.StringUtility;

import java.util.Date;
import java.util.Properties;

/**
 * 自定义注释生成器
 */
public class CommentGenerator extends DefaultCommentGenerator {

    /**
     * 是否添加 swagger 注解
     */
    private boolean addRemarkComments = false;

    /**
     * 作者
     */
    private String author = "javgo";

    /**
     * 生成的 Example 类的结尾名称
     */
    private static final String EXAMPLE_SUFFIX = "Example";

    /**
     * 生成的 Mapper 类的结尾名称
     */
    private static final String MAPPER_SUFFIX = "Mapper";

    /**
     * swagger 注解属性的类全限定名
     */
    private static final String API_MODEL_PROPERTY_FULL_CLASS_NAME = "io.swagger.annotations.ApiModelProperty";

    /**
     * swagger 注解类全限定名
     */
    private static final String API_MODEL_FULL_CLASS_NAME = "io.swagger.annotations.ApiModel";

    /**
     * lombok @Data 注解类全限定名
     */
    private static final String LOMBOK_DATA_FULL_CLASS_NAME = "lombok.Data";

    /**
     * lombok @Builder 注解类全限定名
     */
    private static final String LOMBOK_BUILDER_FULL_CLASS_NAME = "lombok.Builder";

    /**
     * lombok @NoArgsConstructor 注解类全限定名
     */
    private static final String LOMBOK_NOARG_FULL_CLASS_NAME = "lombok.NoArgsConstructor";

    /**
     * lombok @AllArgsConstructor 注解类全限定名
     */
    private static final String LOMBOK_ALLARG_FULL_CLASS_NAME = "lombok.AllArgsConstructor";

    /**
     * 重写父类方法,添加配置属性
     *
     * @param properties 包含配置属性的Properties对象
     */
    @Override
    public void addConfigurationProperties(Properties properties) {
        // 调用父类的 addConfigurationProperties 方法,传递配置属性
        super.addConfigurationProperties(properties);
        // 获取配置属性 "addRemarkComments" 的值,并通过 StringUtility.isTrue 方法转换为 boolean 类型
        // 然后将结果赋值给当前对象的 addRemarkComments 属性
        this.addRemarkComments = StringUtility.isTrue(properties.getProperty("addRemarkComments"));
    }


    /**
     * 添加字段注释
     *
     * @param field              字段对象
     * @param introspectedTable  数据库表对象
     * @param introspectedColumn 数据库列对象
     */
    @Override
    public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
        // 获取字段的注释信息
        String remarks = introspectedColumn.getRemarks();
        // 如果开启了添加注释功能,并且注释信息不为空
        if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
            // 如果注释信息中包含双引号
            if (remarks.contains("\"")) {
                // 将双引号替换为单引号
                remarks = remarks.replace("\"", "'");
            }
            // 在字段上添加 JavaDoc 注释,并设置注释的 value 为处理后的注释信息
            field.addJavaDocLine("@ApiModelProperty(value = \"" + remarks + "\")");
        }
    }


    /**
     * 为 Java 文件添加注释
     *
     * @param compilationUnit 要添加注释的编译单元
     */
    @Override
    public void addJavaFileComment(CompilationUnit compilationUnit) {
        // 调用父类的 addJavaFileComment 方法,为编译单元添加注释
        super.addJavaFileComment(compilationUnit);

        // 获取编译单元的完全限定名
        String fullyQualifiedName = compilationUnit.getType().getFullyQualifiedName();

        // 判断完全限定名是否不包含 MAPPER_SUFFIX 和 EXAMPLE_SUFFIX
        if (!fullyQualifiedName.contains(MAPPER_SUFFIX) && !fullyQualifiedName.contains(EXAMPLE_SUFFIX)) {
            // 如果不包含,则向编译单元添加 API_MODEL_PROPERTY_FULL_CLASS_NAME 类型的导入
            compilationUnit.addImportedType(new FullyQualifiedJavaType(API_MODEL_PROPERTY_FULL_CLASS_NAME));
        }
    }

    /**
     * 添加类级别的注释
     *
     * @param topLevelClass     要添加注释的类
     * @param introspectedTable 数据库表对象
     */
    @Override
    public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
        // 获取类的名称
        String className = topLevelClass.getType().getShortName();
        // 检查类名称是否不以 Mapper 或 Example 结尾
        if (!className.endsWith(MAPPER_SUFFIX) && !className.endsWith(EXAMPLE_SUFFIX)) {
            // 获取表的注释信息
            String remarks = introspectedTable.getRemarks();
            // 如果开启了添加注释功能,并且注释信息不为空
            if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
                // 如果注释信息中包含双引号
                if (remarks.contains("\"")) {
                    // 将双引号替换为单引号
                    remarks = remarks.replace("\"", "'");
                }

                // 添加类的 JavaDoc 注释
                topLevelClass.addJavaDocLine("/**");
                topLevelClass.addJavaDocLine(" * " + remarks);
                topLevelClass.addJavaDocLine(" * @author " + author);
                topLevelClass.addJavaDocLine(" * @date " + new Date());
                topLevelClass.addJavaDocLine(" */");

                // 添加 Lombok 注解
                topLevelClass.addImportedType(new FullyQualifiedJavaType(LOMBOK_DATA_FULL_CLASS_NAME));
                topLevelClass.addImportedType(new FullyQualifiedJavaType(LOMBOK_BUILDER_FULL_CLASS_NAME));
                topLevelClass.addImportedType(new FullyQualifiedJavaType(LOMBOK_NOARG_FULL_CLASS_NAME));
                topLevelClass.addImportedType(new FullyQualifiedJavaType(LOMBOK_ALLARG_FULL_CLASS_NAME));
                topLevelClass.addAnnotation("@Data");
                topLevelClass.addAnnotation("@Builder");
                topLevelClass.addAnnotation("@NoArgsConstructor");
                topLevelClass.addAnnotation("@AllArgsConstructor");

                // 添加 Swagger 的 @ApiModel 注解
                topLevelClass.addImportedType(new FullyQualifiedJavaType(API_MODEL_FULL_CLASS_NAME));
                topLevelClass.addAnnotation("@ApiModel(description = \"" + remarks + "\")");
            }
        }
    }
}

自定义插件去除默认的 Getter And Setter

因为我们已经通过上一个插件生成了 Lombok 注解了,为了避免冲突,应该去掉 MBG 自动为 JavaBean 生成的 Getter 和 Setter 方法:

package cn.javgo.mybatis.generator.plugins;

import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;

import java.util.List;

/**
 * Lombok 插件,用于阻止生成 Getter 和 Setter 方法。
 */
public class NoGetterSetterPlugin extends PluginAdapter  {

    /**
     * 验证列表是否有效
     *
     * @param list 待验证的列表
     * @return 如果列表有效返回true,否则返回false
     */
    @Override
    public boolean validate(List<String> list) {
        // 空实现即可
        return true;
    }

    /**
     * 当生成模型类的 Getter 方法时调用此方法。
     *
     * @param method              生成的 Getter 方法
     * @param topLevelClass       顶层类对象
     * @param introspectedColumn  数据库列对象
     * @param introspectedTable   数据库表对象
     * @param modelClassType    模型类类型
     * @return 如果不生成 Getter 方法,则返回 false;否则返回 true。
     */
    @Override
    public boolean modelGetterMethodGenerated(Method method, TopLevelClass topLevelClass, IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable, ModelClassType modelClassType) {
        return false;
    }

    /**
     * 在生成模型类的 Setter 方法时调用此方法。
     *
     * @param method              生成的 Setter 方法
     * @param topLevelClass       顶层类对象
     * @param introspectedColumn  数据库列对象
     * @param introspectedTable   数据库表对象
     * @param modelClassType    模型类类型
     * @return 如果不生成 Setter 方法,则返回 false;否则返回 true。
     */
    @Override
    public boolean modelSetterMethodGenerated(Method method, TopLevelClass topLevelClass, IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable, ModelClassType modelClassType) {
        return false;
    }
}

MBG 配置文件

在项目的 resources 目录下新建一个 generatorConfig.xml 文件作为 MBG 的配置文件,文件内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration>

    <!-- 引入属性文件(默认查找 CLASSPATH) -->
    <properties resource="generatorConfig.properties"/>

    <!--
        配置模板
        id:配置模板的id,全局唯一
        targetRuntime:配置生成的代码风格
            1. MyBatis3:mybatis3风格,功能完整(推荐)
            2. MyBatis3Simple:mybatis3 简单风格,功能不完整,只有简单的 CRUD
    -->
    <context id="MySql" targetRuntime="MyBatis3">
        <!-- 配置 SQL 语句中的前置分隔符 -->
        <property name="beginningDelimiter" value="`"/>
        <!-- 配置 SQL 语句中的后置分隔符 -->
        <property name="endingDelimiter" value="`"/>
        <!-- 配置生成 Java 文件的编码 -->
        <property name="javaFileEncoding" value="UTF-8"/>

        <!-- 配置插件 -->
        <!-- 自定义注解插件:为生成的 Mapper 接口添加 @Mapper 注解 -->
        <plugin type="cn.javgo.mybatis.generator.plugins.AnnotationPlugin">
            <property name="annotationClass" value="org.apache.ibatis.annotations.Mapper"/>
            <property name="annotationName" value="@Mapper"/>
        </plugin>
        <!-- 自定义注解插件:清除自动生成的 getter/setter 方法,避免与 Lombok 注解冲突 -->
        <plugin type="cn.javgo.mybatis.generator.plugins.NoGetterSetterPlugin"/>
        <!-- 自定义分页插件:为生成的 Example 类添加分页相关的字段和方法 -->
        <plugin type="cn.javgo.mybatis.generator.plugins.PaginationPlugin"/>
        <!-- 覆盖 XML 文件插件:避免生成重复的XML 文件,导致运行时异常 -->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
        <!-- 序列化插件:使生成的实体类实现 Serializable 接口 -->
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>

        <!-- 自定义注释生成器 -->
        <commentGenerator type="cn.javgo.mybatis.generator.internal.CommentGenerator">
            <!-- 是否去除自动生成的注释,默认为 false;设置为 true 则去除 -->
            <property name="suppressAllComments" value="true"/>
            <!-- 是否在生成的注释中包含时间戳,默认为 false;设置为 true 则不包含 -->
            <property name="suppressDate" value="true"/>
            <!-- 是否添加数据库表的备注信息(使用 Swagger 注解) -->
            <property name="addRemarkComments" value="true"/>
            <!-- 指定作者 -->
            <property name="author" value="javgo"/>
        </commentGenerator>

        <!-- 配置数据库连接:
            driverClass:驱动类
            connectionURL:数据库连接地址
            userId:用户名
            password:密码
         -->
        <jdbcConnection driverClass="${db.driver}"
                        connectionURL="${db.url}"
                        userId="${db.username}"
                        password="${db.password}">
            <!-- 解决 MySQL 驱动升级到 8.0 后不生成指定数据库代码的问题 -->
            <property name="nullCatalogMeansCurrent" value="true" />
        </jdbcConnection>

        <!-- 自定义 Java 类型解析器 -->
        <javaTypeResolver type="cn.javgo.mybatis.generator.internal.type.JavaTypeResolverCustomImpl">
            <!-- 是否强制使用 BigDecimal 类型:
                1. true:强制使用 BigDecimal 类型,会将对应数据库中的 DECIMAL 和 NUMERIC 类型映射为 BigDecimal 类型
                2. false:使用 JDBC 的默认类型(根据数据库类型自动转换,默认)
            -->
            <property name="forceBigDecimals" value="true"/>
        </javaTypeResolver>

        <!-- 配置 JavaBean 生成器:
            targetPackage:生成 JavaBean 所在的包名
            targetProject:生成 JavaBean 所存放的路径(需要手动创建)
        -->
        <javaModelGenerator targetProject="${projectPath}/${projectName}/src/main/java"
                            targetPackage="${packagePrefix}.${packageName}.entity">
            <!-- 是否对字符串类型字段调用 trim() 方法,默认为 false;设置为 true 则去除首尾空格 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- 配置 Mapper XML 映射文件生成器:
            targetPackage:Mapper XML 映射文件所在包名(一般推荐跟 Mapper 所在的包名一致,不强制)
            targetProject:Mapper XML 映射文件存放的路径(需要手动创建)
        -->
        <sqlMapGenerator targetPackage="mapper"
                         targetProject="${projectPath}/${projectName}/src/main/resources"/>

        <!-- 配置 Mapper Interface 生成器:
            type:Mapper 接口的类型,默认为 XMLMAPPER,表示使用基于 XML 的映射文件(如果想要使用基于注解的映射文件,请将此属性设置为 ANNOTATEDMAPPER)
            targetPackage:Mapper 接口所在包名(一般推荐跟 Mapper XML 所在的包名一致,不强制)
            targetProject:Mapper 接口存放的路径(需要手动创建)
        -->
        <javaClientGenerator type="XMLMAPPER"
                             targetPackage="${packagePrefix}.${packageName}.mapper"
                             targetProject="${projectPath}/${projectName}/src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 表配置,% 代表生成全部表 -->
        <!-- <table tableName="%"/> -->

        <!-- 或者明确指定需要生成代码的表名 -->
        <table tableName="${tableNames}"/>

        <!-- 也可以针对特定表进行详细配置 -->
        <!-- <table tableName="your_table_name" domainObjectName="YourEntityName"> -->
        <!--     <columnOverride column="your_column_name" jdbcType="VARCHAR"/> -->
        <!-- </table> -->
    </context>
</generatorConfiguration>

⚠️ 注意:上面配置文件中带有 “自定义” 字样的插件其实就是我们前面自定义的插件了,只需要更换对应的 type 为你实际插件的全路径名即可。注释都非常详细了,按照注释理解即可。

MBG 外部属性文件

其中使用到的属性文件 generatorConfig.properties 同样在项目的 resources 目录下创建,内容如下:

# 基本连接信息(如果是 MySQL 8.0 以下版本请将 com.mysql.cj.jdbc.Driver 替换为 com.mysql.jdbc.Driver)
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mybatis_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
db.username=root
db.password=root

# 生成代码相关配置
# 作者
author=javgo
# 生成代码存放路径(即单体项目或者微服务对应模块的父目录)
projectPath=/Users/javgo/it/back-end/java/project/javgo-mybatis
# 项目名称(单体项目名称,或者微服务模块名称)
projectName=mybatis_generator
# 包名(包名前缀)
packagePrefix=cn.javgo
# 包名(包名后缀,一般具有业务含义)
packageName=mybatis
# 表名(多个表名用英文逗号分隔,全部表生成则使用 % 表示)
tableNames=%

在实际使用时,请参照上述注释进行适当调整,避免后面生成代码失败。

Log4j 日志文件

最后同样在项目的 resources 目录下新建一个 log4j.properties 文件作为 Log4j 日志框架的日志文件即可:

# 定义根日志记录器的级别和输出位置
log4j.rootLogger=debug,CONSOLE

# 定义日志输出位置,类型为控制台输出
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender

# 设置 CONSOLE 输出位置的目标为 System.out,即标准输出
log4j.appender.CONSOLE.Target=System.out

# 设置 CONSOLE 输出位置的日志级别为 debug,即 debug 及以上级别的日志都会被输出 (包括 debug、info、warn、error)
log4j.appender.CONSOLE.Threshold=debug

# 设置 CONSOLE 输出位置的日志格式
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout

# [%-5p] 表示日志级别,%-5p 表示左对齐,宽度为 5,如果不足则填充空格
# [%d{yyyy-MM-dd HH:mm:ss,SSS}] 表示日志产生的时间,精确到毫秒
# [%C{1}:%L] 表示产生日志的类名和方法行号,%C{1} 表示只输出类名的最后一个部分,%L 表示输出方法行号
# %m 表示输出日志的具体信息
# %n 表示换行
log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p][%d{yyyy-MM-dd HH:mm:ss,SSS}][%C{1}:%L] %m%n

模块项目结构

OK,截至目前你的项目结构大概如下:

执行 MBG 插件

MBG 也有插件启动的形式,但是实际操作下来我发现使用插件的方式总有一些预期之外的报错,对此我们还可以通过编写代码的方式启动:

package cn.javgo.mybatis.generator;


import lombok.extern.slf4j.Slf4j;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Slf4j
public class Generator {
    public static void main( String[] args ) throws Exception {
        log.info("Generator start ...");

        // 创建一个用于存储警告信息的列表
        List<String> warnings = new ArrayList<>();

        // 设置是否覆盖已存在的文件,这里设置为 true,每次执行都会覆盖已存在的文件,避免重复生成导致代码冲突
        boolean overwrite = true;
        // 创建配置解析器,传入警告信息列表
        ConfigurationParser parser = new ConfigurationParser(warnings);

        // 获取 mybatis-config.xml 配置文件的路径
        File configFile = new File(Objects.requireNonNull(Generator.class.getResource("/generatorConfig.xml")).getFile());
        // 解析配置文件,得到配置信息
        Configuration config = parser.parseConfiguration(configFile);

        // 创建默认的 Shell 回调对象,传入是否覆盖已存在的文件
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        // 创建 MyBatisGenerator 对象,传入配置信息、回调对象和警告信息列表
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        // 执行生成操作,传入 null 表示不指定自定义的进度监听器
        myBatisGenerator.generate(null);

        log.info("Generator end ...");
    }
}

直接执行 main() 方法,就可以开始生成了:

⚠️ 注意:这里遗留了一个问题,那就是其实将 tableNames 配置为 factory,worker 这样的形式时其实是无法生成代码的,因为不支持直接在一个 <table> 标签中指定多个表名,如果有更好的建议的伙伴欢迎在评论区给出你们的思路,进一步完善,从而使得 MBG 通用模块更加灵活。

代码剖析

POJO

package cn.javgo.mybatis.generator.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 工厂信息表
 * @author javgo
 * @date Fri Mar 29 14:20:23 CST 2024
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "工厂信息表")
public class Factory implements Serializable {
    @ApiModelProperty(value = "工厂ID")
    private Integer fId;

    @ApiModelProperty(value = "工厂名称")
    private String fName;

    @ApiModelProperty(value = "状态:1-正常,2-禁用")
    private Integer status;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime gmtCreate;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime gmtUpdate;

    private static final long serialVersionUID = 1L;
}
package cn.javgo.mybatis.generator.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 工人表
 * @author javgo
 * @date Fri Mar 29 14:20:23 CST 2024
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "工人表")
public class Worker implements Serializable {
    @ApiModelProperty(value = "工人ID")
    private Integer wId;

    @ApiModelProperty(value = "工人名称")
    private String wName;

    @ApiModelProperty(value = "工厂ID")
    private Integer fId;

    @ApiModelProperty(value = "状态:1-正常,2-禁用")
    private Integer status;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime gmtCreate;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime gmtUpdate;

    private static final long serialVersionUID = 1L;
}

可以看到生成的 POJO 还是比较全的,但是缺少了构造函数,后面进一步优化时我们会再次进行扩展。其中注意到,我们的日期字段如期的被转换成了 LocalDateTime 类型,数据库中的 tinyint 也被转换为了 Integer

Example

除了基本的 POJO,MBG 害为每个 POJO 生成了对应的 XxxExample 类,用于在执行 CRUD 的时候构造各种筛选条件,其中还包括了一个核心静态内部类 Criteria

Criteria 类的常用方法整理如下:

方法名描述
setDistinct(boolean distinct)配置是否去掉重复记录
setOrderByClause(String orderByClause)设置排序方式
or()添加或(||)条件

Example 类的常用方法整理如下:

方法名描述
andXxxIsNull()添加字段为 null 的条件
andXxxIsNotNull()添加字段不为 null 的条件
andXxxEqualTo(value)添加字段等于 value 的条件
andXxxNotEqualTo(value)添加字段不等于 value 的条件
andXxxGreaterThan(value)添加字段大于 value 的条件
andXxxGreaterThanOrEqualTo(value)添加字段大于等于 value 的条件
andXxxLessThan(value)添加字段小于 value 的条件
andXxxGreaterThanOrEqualTo(value)添加字段小于等于 value 的条件
andXxxIn(List<?>)添加字段值在 List<?> 的条件
andXxxNotIn(List<?>)添加字段值不在 List<?> 的条件
andXxxLike("%" + value + "%")添加字段值为 value 的全模糊查询条件
andXxxNotLike("%" + value + "%")添加字段值不为 value 的全模糊查询条件
andXxxBetween(value1,value2)添加字段值在 [value1,value2] 之间的条件
andXxxNotBetween(value1,value2)添加字段值不在 [value1,value2] 之间的条件

最后再来关注一下我们设置的默认分页字段以及方法也同样符合预期:

如果想要修默认值,只需要修改自定义分页插件 PaginationPlugin 中的 field.setInitializationString("-1") 即可:

如果想调整分页字段名则修改 modelExampleClassGenerated 方法即可:

Mapper Interface

package cn.javgo.mybatis.generator.mapper;

import cn.javgo.mybatis.generator.entity.Factory;
import cn.javgo.mybatis.generator.entity.FactoryExample;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface FactoryMapper {
    int countByExample(FactoryExample example);

    int deleteByExample(FactoryExample example);

    int deleteByPrimaryKey(Integer fId);

    int insert(Factory record);

    int insertSelective(Factory record);

    List<Factory> selectByExample(FactoryExample example);

    Factory selectByPrimaryKey(Integer fId);

    int updateByExampleSelective(@Param("record") Factory record, @Param("example") FactoryExample example);

    int updateByExample(@Param("record") Factory record, @Param("example") FactoryExample example);

    int updateByPrimaryKeySelective(Factory record);

    int updateByPrimaryKey(Factory record);
}

可以看到,MBG 也为我们的 Mapper 接口文件添加了必要的单表 CRUD 操作,基本都是见名知意的。

Mapper XML

最后看看 MBG 生成的 Mapper XML 文件,文件内容较多这里给部分截图,基本就是操作数据库实现 Mapper 接口中的 CRUD 方法:

代码应用

TIP:正常开发中我们一般都是分模块的,这里为了演示方便就不分了,下面简单讲解一下如何快速上手 MBG 为我们自动生成的代码。

SpringBoot 集成配置

要想 Spring Boot 集成 MyBatis,我们在开篇的 pom.xml 文件中已经把相关的依赖准备好了,接下来就是进行一些其他的相关配置。

首先在项目的 resources 目录下新建 application.yml 配置文件,配置一下数据库信息、MyBatis 信息等:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root

mybatis:
  mapper-locations:
    - classpath:mapper/*.xml
    - classpath*:mapper/*.xml

上面只是简单配置示例,在实际的生产环境中你可能还需要对 Druid Spring Boot Starter 进行额外的配置,具体可以参考 Druid Spring Boot Starter 的官方文档,都有详细的生产案例:

然后在项目的启动类或者单独创建一个 MyBatisConfig 配置类通过 @MapperScan 注解添加 Mapper 接口的包路径,以便 Spring Boot 能够进行扫描并注册为 Spring Bean。(这里选择后者)

package cn.javgo.mybatis.generator.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis 配置类
 */
@Configuration
@MapperScan({"cn.javgo.mybatis.generator.mapper"})
public class MyBatisConfig {
}

最后为应用添加一个启动类即可:

package cn.javgo.mybatis.generator;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GeneratorApplication {
    public static void main(String[] args) {
        SpringApplication.run(GeneratorApplication.class, args);
    }
}

通用 API 响应封装

一般而言,我们都需要对响应数据进行统一封装,实现标准化。下面给一个简单的示例参考:

package cn.javgo.mybatis.generator.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * API 响应结果包装类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class ApiResponse<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 默认为系统错误
     */
    private Integer code = ResponseCodeEnum.ERROR.getCode();

    private String message;

    private T data;

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(ResponseCodeEnum.SUCCESS.getCode(), "操作成功", data);
    }

    public static <T> ApiResponse<T> fail(Integer code, String message) {
        return new ApiResponse<>(code, message, null);
    }

    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(ResponseCodeEnum.ERROR.getCode(), message, null);
    }

    public static <T> ApiResponse<T> success() {
        return success(null);
    }
}
package cn.javgo.mybatis.generator.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * API 分页响应包装类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Pagination<T> implements Serializable {

    private static final long serialVersionUID = -5764853545343945831L;

    /**
     * 当前页数据列表
     */
    private List<T> records;

    /**
     * 总记录数
     */
    private int totalRecords;

    /**
     * 总页数
     */
    private int totalPages;

    /**
     * 当前页码
     */
    private int currentPage;

    /**
     * 每页记录数
     */
    private int pageSize;
}
package cn.javgo.mybatis.generator.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 响应状态枚举
 */
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum {

    // 通用成功与错误
    SUCCESS(200, "成功"),
    ERROR(500, "系统错误"),

    // 认证与权限相关
    TOKEN_PAST(401, "Token过期"),
    TOKEN_ERROR(402, "Token异常"),
    LOGIN_ERROR(403, "登录异常"),
    REMOTE_ERROR(404, "异地登录");

    // 业务相关 ...

    private final Integer code;

    private final String message;

    public static ResponseCodeEnum getByCode(Integer code) {
        for (ResponseCodeEnum responseCodeEnum : ResponseCodeEnum.values()) {
            if (responseCodeEnum.getCode().equals(code)) {
                return responseCodeEnum;
            }
        }
        return null;
    }
}

Select 操作

根据主键查询数据:selectByPrimaryKey(id)
package cn.javgo.mybatis.generator.service;

import cn.javgo.mybatis.generator.common.ApiResponse;
import cn.javgo.mybatis.generator.entity.Factory;

public interface IFactoryService {

    ApiResponse<Factory> getItemById(Integer fId);
}
package cn.javgo.mybatis.generator.service.impl;

import cn.javgo.mybatis.generator.common.ApiResponse;
import cn.javgo.mybatis.generator.entity.Factory;
import cn.javgo.mybatis.generator.mapper.FactoryMapper;
import cn.javgo.mybatis.generator.service.IFactoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
@RequiredArgsConstructor
public class FactoryServiceImpl implements IFactoryService {

    private final FactoryMapper factoryMapper;

    @Override
    public ApiResponse<Factory> getItemById(Integer fId) {
        // 主要关注的 API 
        Factory factory = factoryMapper.selectByPrimaryKey(fId);
        return ApiResponse.success(factory);
    }
}
查询所有数据:selectByExample(null)
@Override
public ApiResponse<List<Factory>> list() {
    // 主要关注的 API,方法不传入任何参数即可查询所有数据
    List<Factory> factories = factoryMapper.selectByExample(null);
    return ApiResponse.success(factories);
}
根据条件查询数据:selectByExample(example)
@Override
public ApiResponse<List<Factory>> queryByName(String fName) {
    // 创建 Example 对象
    FactoryExample example = new FactoryExample();
    // 创建内部类 Criteria 对象,添加查询条件
    FactoryExample.Criteria criteria = example.createCriteria();
    // 设置查询条件
    if (fName.startsWith("*")) {
        // 全匹配
        criteria.andFNameLike("%" + fName.substring(1) + "%");
    } else if (fName.endsWith("*")) {
        // 前缀匹配
        criteria.andFNameLike(fName.substring(0, fName.length() - 1) + "%");
    } else {
        // 精确匹配
        criteria.andFNameEqualTo(fName);
    }
    // 执行查询
    return ApiResponse.success(factoryMapper.selectByExample(example));
}
统计满足条件的记录:countByExample(example)
@Override
public ApiResponse<Long> countByCondition(Factory factory) {
    // 创建 Example 对象
    FactoryExample example = new FactoryExample();
    // 创建内部类 Criteria 对象,添加查询条件
    FactoryExample.Criteria criteria = example.createCriteria();
    // 设置查询条件
    if (Objects.nonNull(factory.getFId())) {
        criteria.andFIdEqualTo(factory.getFId());
    }
    if (Objects.isNull(factory.getFId()) && Objects.nonNull(factory.getFName())) {
        String fName = factory.getFName();
        if (factory.getFName().startsWith("*")) {
            // 设置查询条件
            if (fName.startsWith("*")) {
                // 全匹配
                criteria.andFNameLike("%" + fName.substring(1) + "%");
            } else if (fName.endsWith("*")) {
                // 前缀匹配
                criteria.andFNameLike(fName.substring(0, fName.length() - 1) + "%");
            } else {
                // 精确匹配
                criteria.andFNameEqualTo(fName);
            }
        }
    }
    // 执行查询
    long count = factoryMapper.countByExample(example);
    return ApiResponse.success(count);
}

Delete 操作

根据主键删除数据:deleteByPrimaryKey(id)
@Override
public ApiResponse<String> deleteById(Integer fId) {
    int i = factoryMapper.deleteByPrimaryKey(fId);
    return i == 1 ? ApiResponse.success("删除成功") : ApiResponse.fail("删除失败");
}
根据条件删除数据:deleteByExample(example)
@Override
public ApiResponse<String> deleteByCondition(String fName) {
    // 创建 Example 对象
    FactoryExample example = new FactoryExample();
    // 创建内部类 Criteria 对象,添加查询条件
    FactoryExample.Criteria criteria = example.createCriteria();
    // 设置查询条件
    if (fName.startsWith("*")) {
        // 全匹配
        criteria.andFNameLike("%" + fName.substring(1) + "%");
    } else if (fName.endsWith("*")) {
        // 前缀匹配
        criteria.andFNameLike(fName.substring(0, fName.length() - 1) + "%");
    } else {
        // 精确匹配
        criteria.andFNameEqualTo(fName);
    }
    // 执行删除
    int i = factoryMapper.deleteByExample(example);
    return i > 0 ? ApiResponse.success("删除成功") : ApiResponse.fail("没有符合条件的记录");
}

Insert 操作

插入数据:insert(record)

插入方式一共有两种,分别为 insert()insertSelective()。先来看 insert(),如果插入对象的某些属性有值,在进行新增时,会按照预期将属性值插入到对应的字段。但是如果某些属性没有值时,对应插入的字段值将是 null,即便你已经在数据库中显示指定了字段属性有默认值。所以 insert() 方法如果使用不当就会导致数据库中对应字段的默认值失效

@Override
public ApiResponse<String> insert(Factory factory) {
    if (Objects.isNull(factory.getFName()) || factory.getFName().trim().isEmpty()) {
        return ApiResponse.fail("工厂名称不能为空");
    }
    return factoryMapper.insert(factory) == 1 ? ApiResponse.success("新增成功") : ApiResponse.fail("新增失败");
}
选择性插入数据:insertSelective(record)

insertSelective() 方法则是我们推荐的方式,当出现某些属性值未设置时,在进行新增时就会忽略对应的字段,不会使数据库中的默认值失效。

@Override
public ApiResponse<String> insertSelective(Factory factory) {
    return factoryMapper.insertSelective(factory) == 1 ? ApiResponse.success("新增成功") : ApiResponse.fail("新增失败");
}

Update 操作

根据主键更新数据:updateByPrimaryKey(record)

updateByPrimaryKey(record) 进行更新时同样会出现插入类似的问题,如果某个字段为空将会传入 null,从而影响字段属性(如默认值,或者是否非 NULL)。

@Override
public ApiResponse<String> updateById(Factory factory) {
    if (Objects.isNull(factory.getFId())) {
        return ApiResponse.fail("工厂ID不能为空");
    }
    return factoryMapper.updateByPrimaryKey(factory) == 1 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
}
根据主键选择性更新数据:updateByPrimaryKeySelective(record)

为了避免 updateByPrimaryKey(record) 带来的问题,我们可以使用 updateByPrimaryKeySelective(record) 只更新有值的属性即可。

@Override
public ApiResponse<String> updateSelectiveById(Factory factory) {
    if (Objects.isNull(factory.getFId())) {
        return ApiResponse.fail("工厂ID不能为空");
    }
    return factoryMapper.updateByPrimaryKeySelective(factory) == 1 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
}
根据条件更新数据:updateByExample(row,example)
@Override
public ApiResponse<String> updateByCondition(Factory factory) {
    // 创建 Example 对象
    FactoryExample example = new FactoryExample();
    // 创建内部类 Criteria 对象,添加查询条件
    FactoryExample.Criteria criteria = example.createCriteria();
    // 设置更新需要的条件:以只更新 fName 以 A 开头的工厂
    criteria.andFNameLike("A%");
    // 执行更新
    return factoryMapper.updateByExample(factory, example) == 1 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
}
根据条件选择性更新数据:updateByExampleSelective(row,example)
@Override
public ApiResponse<String> updateSelectiveByCondition(Factory factory) {
    // 创建 Example 对象
    FactoryExample example = new FactoryExample();
    // 创建内部类 Criteria 对象,添加查询条件
    FactoryExample.Criteria criteria = example.createCriteria();
    // 设置更新需要的条件:以只更新 fName 以 A 开头的工厂
    criteria.andFNameLike("A%");
    // 执行选择更新(只更新有值的字段)
    return factoryMapper.updateByExampleSelective(factory, example) == 1 ? ApiResponse.success("更新成功") : ApiResponse.fail("更新失败");
}

总结

OK,到此本文就结束了,如果觉得对你有帮助,可以一键三连支持一下 🤔。

回顾一下我们本文的内容:

  1. MBG 通用模块搭建 + 版本选择思路
  2. 个性化插件定制
  3. MyBatis 与 Spring Boot 集成步骤
  4. MBG 生成的代码基本操作

对应的代码,我上传到了 Gitee 仓库,感兴趣的可以自行参考:https://gitee.com/javgo/javgo-mybatis/tree/main/mybatis_generator


参考资料:

  • https://mybatis.org/generator/
  • https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
  • https://www.163.com/dy/article/GL2Q2O1C0552L3N2.html
  • https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默的老李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值