纵享丝滑,Mybatis Generator 代码持续生成实践
2021/09/09 · vran

前言

Mybatis Generator 是与 Mybatis 配套的代码生成工具,它可以基于 Database 的元数据自动生成实体、Java Mapper 以及 XML 等 Boilerplate code

但目前很多实践都只是在项目初期使用该工具,后面由于文件冲突、功能扩展等原因都不会再使用它生成代码,这种不可持续性让 Mybatis Generator 的价值大打折扣。

本文则分享了我的一些实践和尝试,让 Mybatis Generator 的代码生成在项目的全生命周期内得以持续,希望能为你带来一些灵感。

项目源码请通过 Github 查看。

code

目标

我期望的是只要每次数据库有变动都可以使用 Mybatis Generator 来生成代码,而不仅仅是项目首次初始化使用。要实现这样的目标就得解决代码重复生成时可能引入冲突生成的代码功能不满足需求等问题

也就是说项目得有以下几个特性

  1. 隔离:自动生成的代码能够以某种形式与手写的代码隔离
  2. 扩展性:用户不需要(也不允许)去修改生成的代码
    • 可以修改代码生成的规则,比如通过修改配置或扩展代码生成工具等
    • 能够以某种非侵入性的方式去扩展生成的代码的功能
  3. 一致性:生成的代码结构只和项目有关,而不会受系统环境、构建工具等外部依赖的影响

接下来就正式开始尝试把

集成 Gradle

由于 Mybatis Generator 官方没有提供 Gradle Plugin,所以我选择了通过社区开源的 Gradle 插件 Mybatis Generator Plugin 来集成。

集成方式很简单,首先需要在 build.gradle 中引入一下插件,并配置指定的版本

plugins {
  id "com.thinkimi.gradle.MybatisGenerator" version "2.3"
}

刷新完 Gradle 以后,我们就可以在 build.gradle 中通过 mybatisGenerator 块来配置运行时依赖的 JDBC 驱动、mybatis-generator-core 的版本以及代码生成的规则文件

mybatisGenerator {
    verbose = true
    configFile = 'src/main/resources/mybatis-generator.xml'

    dependencies {
        mybatisGenerator group: 'org.mybatis.generator', name: 'mybatis-generator-core', version: '1.4.0'
        mybatisGenerator group: 'mysql', name: 'mysql-connector-java', version: '8.0.25'
    }
}

mybatis-generator.xml 就是 Mybatis Generator 依赖的规则配置,在该文件中可以配置

  • 生成的代码类型,比如 Mybatis3,Mybatis3Simple 和 Mybatis3DynamicSql 等
  • 数据库的 URL、用户名、密码等
  • 数据库字段与对象字段的映射规则
  • 生成的代码文件存放路径
  • ……

更多详细的配置请参考官方文档 ,下面展示了一个简单的 Demo 配置,完整配置示例可以通过 GitHub 查看

<!DOCTYPE generatorConfiguration PUBLIC
        "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="simple" targetRuntime="MyBatis3">
    	<!-- 数据库相关配置 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/user" userId="root" password="123456"/>

        <!-- 生成代码存放路径和包名 -->
        <javaModelGenerator targetPackage="cc.cc1234.generate.model" targetProject="src/main/java"/>
        <javaClientGenerator type="ANNOTATEDMAPPER" targetPackage="cc.cc1234.generate.mapper" targetProject="src/main/java"/>

        <!-- 表与实体的映射关系配置 -->
        <table tableName="user" domainObjectName="UserEntity">
    		<!-- 字段类型映射 -->
            <columnOverride column="gender" javaType="cc.cc1234.enums.Gender"
                            typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
        </table>
    </context>
</generatorConfiguration>

这样配置完成后,Mybatis Generator Plugin 会自动创建一个名为 mbGenerator 的 Gradle Task,执行该 Task 就能生成对应的 Entity 和 Mapper 代码文件,我们将这部分通过工具自动生成的代码文件称之为 Generated Code。

gradle-task

Generated Code 的隔离

目前所有的 Generated Code 都是存放在一个 targetProject (src/main/java)下,通过包路径来区分这些文件是不是由工具自动生成的,比如在 cc.cc1234.generate 下的代就是自动生成的。

通过包路径的方式来隔离 Generated Code 这种方式可以再优化一下,我期望是达到一种形散神不散的组织方式,这就需要用到源码目录来做隔离。

那什么是源码目录呢?一般默认的 Java 目录结构如下

project
└── src
    ├── main
       ├── java
       └── resources
    └── test
        ├── java
        └── resources

这里的 main 就是源码目录,我要做的就是创建一个和 main 同级的目录专门用来存放 Generated Code,该目录的名称为 generator

新的项目结构就变成了下面这样

project
└── src
    ├── generator
       ├── java
       └── resources
    ├── main
       ├── java
       └── resources
    └── test
        ├── java
        └── resources

generator 目录需要做到以下两点

  1. generator 能够引用到 project 项目下的依赖
  2. main 下的代码能引用到 generator 下的代码

在 Gradle 中只需要在 build.gradle 中配置 SourceSet 就可以轻松实现

sourceSets {
    // 将 generator 目录加入 main 所属的源码目录集合中去
    main {
        java.srcDirs += ['src/generator/java']
        resources.srcDirs += ['src/generator/resources']
    }
}

接下来,我们只需要保证 Mybatis Generaor 生成的代码在 src/generator/java 目录下即可。

但这里直接将 XML 配置中的 targetProject 改成 src/generator/java 是没有用的,生成代码的时候会报错

[ant:mbgenerator] Cannot create directory {user.home}/.gradle/daemon/7.1/src/generator/java/cc/cc1234/dao/mapper

看来插件这一块还是有问题的,好在插件可以引用系统变量,通过系统变量将 targetProject 配置成绝对路径就可以解决这个问题。

首先修改 dao/build.gradle,配置系统变量(注意 System.setProperty 这一行)

mbGenerator.dependsOn {
    System.setProperty("mybatis.generator.baseDir", projectDir.getAbsolutePath())
}

再修改 XML 中的 targetProject 属性,通过 ${mybatis.generator.baseDir} 引用变量即可

<javaModelGenerator targetPackage="cc.cc1234.dao.model" targetProject="${mybatis.generator.baseDir}/src/generator/java"/>

<javaClientGenerator type="ANNOTATEDMAPPER" targetPackage="cc.cc1234.dao.mapper" targetProject="${mybatis.generator.baseDir}/src/generator/java"/>

最后执行 mbGenerator 任务, 这样就完成了将 Generated Code 通过源码目录隔离开了。

为了保证每次生成的代码都是最新的,不会有遗留的一些文件,我为 mbGenerator 添加了一个前置处理逻辑:先删除目录下已生成的旧代码

mbGenerator.dependsOn {
    cleanGeneratedCode
}

task cleanGeneratedCode(type: Delete) {
  	System.setProperty("mybatis.generator.baseDir", projectDir.getAbsolutePath())
    file("src/generator/java").list().each {
        f -> delete "src/generator/java/${f}"
    }
    file("src/generator/resources").list().each {
        f -> delete "src/generator/resources/${f}"
    }
}

Generated Code 的扩展

生成的代码必然是无法满足复杂多变的业务场景的,这就需要有能在不修改 Generated Code 的前提下扩展功能的能力,在这里分为两种

  1. 扩展代码生成规则
  2. 无侵入式的扩展 Generated Code

在本文中其实就是

  1. 扩展 Mybatis Generator 的代码生成能力
  2. 扩展已生成的 Mapper 功能。

Mybatis Generator 的扩展

Mybatis Generator 提供很丰富的扩展点,我这里就不赘述了,读者可以通过官方文档详细了解。

在我的 Demo 项目中,我基于它的扩展机制实现了以下几个插件,实现逻辑请通过 Github 查看

  1. Model 类使用 @Data 注解替代 getter 和 setter
  2. 全局禁用 Example 方法和实体的生成
  3. 默认为 InsertInsertSelective 方法生成 GenerateKeys 配置
  4. 修改 selectByPrimaryKey 方法返回类型为 Optional

验证完 Mybatis Generator 的扩展能力以后,下面就该验证一下 Mapper 类的扩展了。

Mapper 的扩展

自动生成的 Mapper 类提供了简单的 CRUD 函数

  • deleteByPrimaryKey
  • insert
  • insertSelective
  • selectByPrimaryKey
  • updateByPrimaryKeySelective
  • updateByPrimaryKey

我刻意禁用了 XML 文件的生成,这些函数都是使用 Mybatis 自带的 Annotation 和 SqlProvider 实现的。

如果提供的函数无法满足需求,这时候就可以通过继承该 Mapper 来进行扩展,就像这样

interface UserMapper extends UserGeneratedMapper{
    
}

自定义 CRUD 既可以使用 Annotation,也可以使用传统的 XML

interface UserMapper extends UserGeneratedMapper{
    
    /**
    * 使用 Annotation 写 SQL
    */
    @Select(value = "select * from user where deleted = false")
    List<UserEntity> selectAll();
    
    /**
    * 使用 XML 写 SQL
    */
    UserEntity selectByUsername(String username);

    /**
    * 使用 XML 写 SQL
    */
    long countUser();
    
}

对应的 XML

<?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="cc.cc1234.dao.mapper.UserMapper">

    <select id="countUser" resultType="java.lang.Long">
        select count(*)
        from user
    </select>

    <select id="selectByUsername" resultType="cc.cc1234.dao.model.UserEntity">
        select *
        from user
        where username = #{username}
    </select>

</mapper>

在 Mybatis 中 Model 对象仅仅用作承载数据,很少需要去扩展它的能力,所以这里就不考虑它的扩展方式了。

Generated Code 的一致性

完成了代码生成、隔离、扩展性的验证以后,接下来又得考虑一个团队开发时的问题:如何确保每个团队成员生成的代码是一致的?

由于 Generated Code 是基于 Database 的 Schema 来生成的,所以只要团队成员能共享一份 Schema 其实就能保证生成的代码是一致的了。

而这里就有两个方案

  • 在生成代码时,成员使用一个共享的 Database,所有的数据库结构变更都应该先应用到该 Database

  • 在项目中维护 Database 的 Schema 变更操作记录,成员可以通过重放历史变更操作得到最新一份最新的 Schema

这两种方案并不是互斥的,如果条件允许的话,完全可以结合起来应用。

对于维护 Database 的 Schema 变更记录,目前社区已经有了很多成熟的产品可以开箱即用,我这里选择了 Flyway

Flyway 将 Schema 的变更操作称之为 Migration,每个 Migration 是一个有版本标识的 SQL 文件,像下面这样

migration
├── v1__init.sql
├── v2_1__user_table.sql
├── v2_2__order_table.sql
└── v3__order_item_table.sql

Flyway 会自动应用这些 SQL 文件,并记录当前使用到的版本。

这样,成员就算在本地开发也可以得到一个与当前生产环境一致的 Database Schema 了。

integration-flyway

如果你的生产不允许通过 Flyway 来应用 SQL 的话,那么你可以选择在生产环境禁用Flyway,但是在本地和开发环境继续启用。

关于 Flyway 的集成细节的话这里就不展示了,可以直接通过 Github 查看项目配置

总结

到目前为止, 基本实现了使用 Mybatis Generator 来做代码的持续生成。

不过目前的优化点其实还蛮多的

  • 在生成的 Mapper 中已经为实体定义了 ResultMap,但是没有生成 ID,后续可以考虑生成一个 ID,这样扩展的 XML 里就不需要再重复写 ResultMap,直接引用 ID 即可
  • 提供一个更通用的 CRUD 基础 Mapper 的实现

还有,XML 目前不能自动生成的,因为文件的映射是和类作了强绑定,限制了扩展能力。

如果要支持生成 XML 的话,目前有两个想法(没有深入思考,有空了再尝试一下吧)

  • XML 中 mapper.namespce 指向一个通用路由 Mapper
  • 一个 Mapper 可以对应多个 XML 文件,在 Mybatis 启动时做合并

对了,Mybatis Generator 也支持生成 Mybatis Dynamic SQL 的代码,可以参考我上一篇文章《纵享丝滑,Mybatis-Dynamic-Sql 集成体验》

附录

  1. Mybatis continuous generate integration demo
  2. Mybatis Generator docs
  3. Mybatis docs
  4. Mybatis Generator Plugin
  5. Flyway
over