定制MyBatis Generator减少mybatis的使用负担

MyBatis Generator

MyBatis Generator 是MyBatis的快速代码生成工具,它将为MyBatis的所有版本生成代码(entity, 基础的CRUD方法的dao, mapper)。尽管已经提供了大量的配置标签,但是每个公司都有自己的代码规范, 那就只能自己上手扩展了。

一.MyBatis Generator可以通过以下方式运行

  • 用命令行运行MBG
  • 使用ant运行
  • 作为Maven插件
  • 使用Java运行
  • 作为Eclipse的插件

二. Mybatis Generator使用

  1. 依赖
        
            org.mybatis.generator
            mybatis-generator-core
            1.4.0
        

        
            mysql
            mysql-connector-java
            8.0.16
        
  1. 配置文件





    
        
        


        
            
            
            
        

        
        
            
            
        

        
        
            
        

        
        
            
            
            
        

        
        
            
        


        
            

            

            
            

        
  1. 执行,生成代码
    把第二步的xml文件作为configFile传入
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = null;
        myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);

三.MyBatis Generator XML配置介绍

配置文件告诉MBG:

  • 如何连接数据库
  • 为哪些表生成对象
  • 生成什么对象(model,client,map xml),以及如何生成它们

GeneratorXML配置文件:

1.

一个context可以看作一个数据库环境。
可以在 元素内列出多个元素, 以允许在MyBatis Generator的同一运行中从不同的数据库或具有不同的生成参数生成对象。

2. 或者

设置数据库连接。

3.

生成crud,可以设置生成的文件放在哪个包

3.

生成entity,可以设置生成的文件放在哪个包
一些主要功能

  • 是否生成全字段构造函数
  • 是否immutable,将决定是否生成set方法
  • set方法是否自动trim

4.

用于定义SQL映射生成器的属性,当选择的javaClientGenerator需要XML时,此元素才是元素的必需子元素

5.

用于定义 Java 类型解析器的属性。Java 类型解析器决定了 数据库列 与 Java类型的对应关系

6.

定义注释生成器的属性

7.

设置entity类名生成规则
比如当库中的所有表都有一个公共前缀,但是我们的entity名字里面不想要这个前缀

8.

指定表,为该表生成代码(entity, crud方法, mapper)
一些主要配置项:

  • entity的名字
  • 是否使用真实列名作为属性名(否则就用驼峰)
  • 是否生成根据主键的select|delete|update
  • 是否生成插入语句
  • 是否允许主键查询
  • 是否immutable,将决定是否生成set方法
8.1

用于指定自动生成的字段(比如自增id)
如果指定此元素,则MyBatis Generator(MBG)将在SQL映射的生成的元素内生成一个适当的
元素(根据sqlStatement不同而不同),insert的时候会把这个值赋值到对象中。

MySql -> SELECT LAST_INSERT_ID()

8.2.

  • property设置java属性名(一般不需要在columnOverride这个标签里面设置,可以在table标签统一设置成驼峰)
  • 制定列与属性的映射关系
  • isGeneratedAlways=true 生成的insert update方法就不会去设置这个字段的值。
  • typeHandler当默认的类型转换不能满足要求的时候可以自定义类型转换
8.3.

忽略某列,所有生成的代码里面都不会包含这列。

8.4.

根据正则忽略列,可以指定except,这样正则就可以写得更加简单了。

 
8.5.

设置entity中属性名生成规则
比如当表中的所有列都有一个公共前缀,但是我们的entity里面不想要这个前缀。

四.目前使用默认的generator存在的问题

看默认生成的sql和方法,存在一些问题,不太好用

public interface MbgTestMapper {
    @Delete({
        "delete from mbg_test",
        "where id = #{id,jdbcType=BIGINT}"
    })
    int deleteByPrimaryKey(Long id);

    @Insert({
        "insert into mbg_test (status, ",
        "version, created_time, ",
        "another_status, ",
        "msg, long_msg)",
        "values (#{status,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler}, ",
        "#{version,jdbcType=INTEGER}, #{createdTime,jdbcType=TIMESTAMP}, ",
        "#{anotherStatus,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler}, ",
        "#{msg,jdbcType=LONGVARCHAR}, #{longMsg,jdbcType=LONGVARCHAR})"
    })
    @SelectKey(statement="SELECT LAST_INSERT_ID()", keyProperty="id", before=false, resultType=Long.class)
    int insert(MbgTest record);

    @Select({
        "select",
        "id, status, version, created_time, another_status, msg, long_msg",
        "from mbg_test",
        "where id = #{id,jdbcType=BIGINT}"
    })
    @Results({
        @Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
        @Result(column="status", property="status", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="version", property="version", jdbcType=JdbcType.INTEGER),
        @Result(column="created_time", property="createdTime", jdbcType=JdbcType.TIMESTAMP),
        @Result(column="another_status", property="anotherStatus", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="msg", property="msg", jdbcType=JdbcType.LONGVARCHAR),
        @Result(column="long_msg", property="longMsg", jdbcType=JdbcType.LONGVARCHAR)
    })
    MbgTest selectByPrimaryKey(Long id);

    @Select({
        "select",
        "id, status, version, created_time, another_status, msg, long_msg",
        "from mbg_test"
    })
    @Results({
        @Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
        @Result(column="status", property="status", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="version", property="version", jdbcType=JdbcType.INTEGER),
        @Result(column="created_time", property="createdTime", jdbcType=JdbcType.TIMESTAMP),
        @Result(column="another_status", property="anotherStatus", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="msg", property="msg", jdbcType=JdbcType.LONGVARCHAR),
        @Result(column="long_msg", property="longMsg", jdbcType=JdbcType.LONGVARCHAR)
    })
    List selectAll();

    @Update({
        "update mbg_test",
        "set status = #{status,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler},",
          "version = #{version,jdbcType=INTEGER},",
          "created_time = #{createdTime,jdbcType=TIMESTAMP},",
          "another_status = #{anotherStatus,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler},",
          "msg = #{msg,jdbcType=LONGVARCHAR},",
          "long_msg = #{longMsg,jdbcType=LONGVARCHAR}",
        "where id = #{id,jdbcType=BIGINT}"
    })
    int updateByPrimaryKey(MbgTest record);
}

1. 缺少乐观锁支持,导致生成的update,delete方法,容易被误用。

自定义pluginAdater,生成updateByVersion方法,deleteByVersion方法,原有的update/delete方法就不要生成了,防止误用。
自动生成的代码如下

    @Update({
    "update mbg_test",
    "set status = #{status,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler},",
    "version = version+1,",
    "created_time = #{createdTime,jdbcType=TIMESTAMP},",
    "another_status = #{anotherStatus,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler},",
    "msg = #{msg,jdbcType=LONGVARCHAR},",
    "long_msg = #{longMsg,jdbcType=LONGVARCHAR}",
    "where id = #{id,jdbcType=BIGINT}",
    "and version = #{version,jdbcType=BIGINT}"
    })
    int updateByIdAndVersion(MbgTest record);

但是这个方法并不会抛出异常,可以考虑再自动生成default方法来处理异常(具体异常类可以在配置文件里面自由修改)

2. BLOB字段支持还不友好,我们还需要自己写select/update方法 (分别需要实现 带blob字段和不带blob字段的方法)

自定义pluginAdater
目前这里只做了生成一个不含blob字段的select语句。可以考虑生成一套不含blob字段的select/update方法。返回值的问题还没想好(是再生成一个不含blob字段的entity还是生成一个dto,或者说直接使用全字段的entity)

static final String SELECT_FIELDS_WITHOUT_BLOB = "select status, version, created_time, another_status from mbg_test ";

3. 缺少批量操作的方法

自定义pluginAdater后生成代码如下

@Insert({
    ""
    })
    int insertAll(@Param("records") List records);

4. 生成方法的名字不太符合我们的风格

public class CustomIntrospectedTableMyBatis3Ipl extends IntrospectedTableMyBatis3SimpleImpl {

    @Override
    protected void calculateXmlAttributes() {
        super.calculateXmlAttributes();
        setSelectByPrimaryKeyStatementId("findById");
        setDeleteByPrimaryKeyStatementId("delete");
        setUpdateByPrimaryKeyStatementId("update");
    }
}

5. LocalDateTime支持

public class CustomJavaTypeResolver extends JavaTypeResolverDefaultImpl {

    public CustomJavaTypeResolver() {
        super();
        typeMap.put(Types.TIMESTAMP, new JdbcTypeInformation("TIMESTAMP",
                new FullyQualifiedJavaType(LocalDateTime.class.getName())));
    }
}

6. 写别的方法的时候不想写select *的话就得把字段给拷贝一遍,不方便维护(表上改了,代码忘记改了或者改漏了)

自定义pluginAdater,使得把字段给提取一个变量出来,后面自己写方法的时候就可以引用了。
生成的代码:

static final String SELECT_ALL_FIELDS = "select id, status, version, created_time, another_status, msg, long_msg from mbg_test ";

7. 枚举类的支持

mybatis提供了自定义的typeHandler。在使用mybatis generator的时候指定typeHandler即可。

8. 期望可以生成一个resultMap的id"), 不用再写一遍映射关系

自定义pluginAdater,在findById方法生成后,修改一下方法的annotation加上id
扩展后自动生成代码如下

@Select({
        "select",
        "id, status, version, created_time, another_status, msg, long_msg",
        "from mbg_test",
        "where id = #{id,jdbcType=BIGINT}"
    })
    @Results(value = {
        @Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
        @Result(column="status", property="status", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="version", property="version", jdbcType=JdbcType.BIGINT),
        @Result(column="created_time", property="createdTime", jdbcType=JdbcType.TIMESTAMP),
        @Result(column="another_status", property="anotherStatus", typeHandler=IntEnumTypeHandler.class, jdbcType=JdbcType.TINYINT),
        @Result(column="msg", property="msg", jdbcType=JdbcType.LONGVARCHAR),
        @Result(column="long_msg", property="longMsg", jdbcType=JdbcType.LONGVARCHAR)
    }, id="mbgTest")
    MbgTest findById(Long id);

五.源码简单解读

1. 最外层,读取 然后挨个处理

一个context对应一个数据库

// 读取 然后挨个处理
contextsToRun = configuration.getContexts();

// methods related to code generation.
// Methods should be called in this order:
// 1.获取分析步骤(对于每个context来说有两步,1连接数据库,2分析表)
context.getIntrospectionSteps();
// 2.分析需要生成代码的相关表
context.introspectTables(callback, warnings, fullyQualifiedTableNames);
// 3.获取生成文件的步骤
context.getGenerationSteps();
// 4.获取需要生成的文件
// 每个context处理好后会把结果放到generatedJavaFiles,generatedXmlFiles,generatedKotlinFiles三个列表中
context.generateFiles(callback, generatedJavaFiles,
                    generatedXmlFiles, generatedKotlinFiles, warnings);

// 写文件
for (GeneratedXmlFile gxf : generatedXmlFiles) {
    projects.add(gxf.getTargetProject());
    writeGeneratedXmlFile(gxf, callback);
}
    
for (GeneratedJavaFile gjf : generatedJavaFiles) {
    projects.add(gjf.getTargetProject());
    writeGeneratedJavaFile(gjf, callback);
}
    
for (GeneratedKotlinFile gkf : generatedKotlinFiles) {
    projects.add(gkf.getTargetProject());
    writeGeneratedKotlinFile(gkf, callback);
}

2. 分析表introspectTables

// 1.获取类型处理器
JavaTypeResolver javaTypeResolver = ObjectFactory
                .createJavaTypeResolver(this, warnings);
// 2.获取数据库分析器,分析表
DatabaseIntrospector databaseIntrospector = new DatabaseIntrospector(
                    this, connection.getMetaData(), javaTypeResolver, warnings);
databaseIntrospector.introspectTables(tc);                    

// 分析字段,包括1. 排除忽略字段,2.获取列的元数据,并根据元数据得出对应的java类型和属性名 3.应用的设置 4.处理字段相关
removeIgnoredColumns(tc, columns);
calculateExtraColumnInformation(tc, columns);
applyColumnOverrides(tc, columns);
calculateIdentityColumns(tc, columns);

// 分析表的元数据,并且得到类名,主键信息等
List introspectedTables = calculateIntrospectedTables(
                tc, columns);

3. 生成文件内容generateFiles(以client生成为例)

ps:官方提供了PluginAdater,调用Plugin的地方都是我们可以插入自己逻辑的地方

// 1.获取插件列表,plugin.validate为true的才会加入
        pluginAggregator = new PluginAggregator();
        Plugin plugin = ObjectFactory.createPlugin(this,
                    pluginConfiguration);
                    
        // 2.生成interface
        Interface interfaze = new Interface(type);
        
        // 3. 生成基本方法
        addDeleteByPrimaryKeyMethod(interfaze);
        addInsertMethod(interfaze);
        addSelectByPrimaryKeyMethod(interfaze);
        addSelectAllMethod(interfaze);
        addUpdateByPrimaryKeyMethod(interfaze);

        // 4. 调用Plugin的方法(这个时候我们可以在plugin里面往interfaze上面挂任何我们想要的代码)
        if (context.getPlugins().clientGenerated(interfaze, introspectedTable)) {
            answer.add(interfaze);
        }

4. 官方提供的methodGenerator

// 1.生成方法
 Method method = new Method(introspectedTable.getDeleteByPrimaryKeyStatementId());

// 2.annotation
addMapperAnnotations(method);

// 3.调用plugin对应的方法(我们可以在这里修改这个method,或者阻止这个method生成)
if (context.getPlugins().clientDeleteByPrimaryKeyMethodGenerated(
                method, interfaze, introspectedTable)) {
            //  4.添加import
            addExtraImports(interfaze);
            interfaze.addImportedTypes(importedTypes);
            // 5.把这个方法挂到interface上面
            interfaze.addMethod(method);
}

六.定制mybatis generator的切入点---PluginAdapter

PluginAdapter提供了丰富的逻辑切入点,这些方法会在mybatis generator的各个环节被触发。而且运行的上下文环境(context),配置(properties)会被注入其中。

你可能感兴趣的:(定制MyBatis Generator减少mybatis的使用负担)