定制MyBatis Generator减少mybatis的使用负
MyBatis Generator
MyBatis Generator 是MyBatis的快速代码生成工具,它将为MyBatis的所有版本生成代码(entity, 基础的CRUD方法的dao, mapper)。尽管已经提供了大量的配置标签,但是每个公司都有自己的代码规范, 那就只能自己上手扩展了。
一.MyBatis Generator可以通过以下方式运行
- 用命令行运行MBG
- 使用ant运行
- 作为Maven插件
- 使用Java运行
- 作为Eclipse的插件
二. Mybatis Generator使用
- 依赖
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
- 配置文件
<?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>
<!-- <context id="sqlserverTables" targetRuntime="IntrospectedTableMyBatis3CustomIpl">-->
<context id="sqlserverTables" targetRuntime="MyBatis3Simple">
<!-- 生成的pojo,将implements Serializable-->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
<!-- <plugin type="com.myproject.mbg.CustomPlugin" />-->
<commentGenerator>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true" />
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!-- 数据库链接URL、用户名、密码 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://ip/库名"
userId="***"
password="***">
<property name="useInformationSchema" value="true"/>
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<!--
默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer
true,把JDBC DECIMAL 和 NUMERIC 类型解析为java.math.BigDecimal
-->
<javaTypeResolver type="com.myproject.mbg.CustomJavaTypeResolver">
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!--
生成model模型,对应的包路径,以及文件存放路径(targetProject),targetProject可以指定具体的路径,如./src/main/java,
也可以使用“MAVEN”来自动生成,这样生成的代码会在target/generatord-source目录下
-->
<javaModelGenerator targetPackage="com.myproject.entity" targetProject="myproject/src/main/java">
<property name="enableSubPackages" value="true"/>
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- 对应的Mapper接口类文件 -->
<javaClientGenerator targetPackage="com.myproject.dao.mapper" targetProject="myproject/src/main/java" type="ANNOTATEDMAPPER">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<table tableName="mbg_test" domainObjectName="MbgTest" enableSelectByExample="true" modelType="flat">
<property name="useActualColumnNames" value="false"/>
<generatedKey column ="id" sqlStatement ="MYSQL" identity="true"/>
<!-- <columnOverride column="id" isGeneratedAlways="true"/>-->
<ignoreColumn column="test_ignore" />
<columnOverride column="status" javaType="com.myproject.entity.enums.MbgTestStatus" typeHandler="com.myproject.core.mybatis.typehandler.IntEnumTypeHandler" />
<columnOverride column="another_status" javaType="com.myproject.entity.enums.AnotherStatus" typeHandler="com.myproject.core.mybatis.typehandler.IntEnumTypeHandler" />
</table>
</context>
</generatorConfiguration>
- 执行,生成代码
把第二步的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>
一个context可以看作一个数据库环境。
可以在<generatorConfiguration> 元素内列出多个<context>元素, 以允许在MyBatis Generator的同一运行中从不同的数据库或具有不同的生成参数生成对象。
2. <connectionFactory> 或者 <jdbcConnection>
设置数据库连接。
3. <javaClientGenerator>
生成crud,可以设置生成的文件放在哪个包
3. <javaModelGenerator>
生成entity,可以设置生成的文件放在哪个包
一些主要功能
- 是否生成全字段构造函数
- 是否immutable,将决定是否生成set方法
- set方法是否自动trim
4. <sqlMapGenerator>
用于定义SQL映射生成器的属性,当选择的javaClientGenerator需要XML时,此元素才是<context>元素的必需子元素
5. <javaTypeResolver>
用于定义 Java 类型解析器的属性。Java 类型解析器决定了 数据库列 与 Java类型的对应关系
6. <commentGenerator>
定义注释生成器的属性
7. <domainObjectRenamingRule>
设置entity类名生成规则
比如当库中的所有表都有一个公共前缀,但是我们的entity名字里面不想要这个前缀
8. <table>
指定表,为该表生成代码(entity, crud方法, mapper)
一些主要配置项:
- entity的名字
- 是否使用真实列名作为属性名(否则就用驼峰)
- 是否生成根据主键的select|delete|update
- 是否生成插入语句
- 是否允许主键查询
- 是否immutable,将决定是否生成set方法
8.1 <generatedKey>
用于指定自动生成的字段(比如自增id)
如果指定此元素,则MyBatis Generator(MBG)将在SQL映射的生成的<insert>元素内生成一个适当的<selectKey>
元素(根据sqlStatement不同而不同),insert的时候会把这个值赋值到对象中。
MySql -> SELECT LAST_INSERT_ID()
8.2. <columnOverride>
<columnOverride column="sku_name" property="skuName" javaType="java.lang.String" jdbcType="VARCHAR"
isGeneratedAlways="false" typeHandler="com.***.***TypeHandler"/>
- property设置java属性名(一般不需要在columnOverride这个标签里面设置,可以在table标签统一设置成驼峰)
- 制定列与属性的映射关系
- isGeneratedAlways=true 生成的insert update方法就不会去设置这个字段的值。
- typeHandler当默认的类型转换不能满足要求的时候可以自定义类型转换
8.3. <ignoreColumn>
忽略某列,所有生成的代码里面都不会包含这列。
8.4. <ignoreColumnsByRegex>
根据正则忽略列,可以指定except,这样正则就可以写得更加简单了。
<table tableName="Foo">
<ignoreColumnsByRegex pattern="(?i)col.*">
<except column="col01"/>
<except column="col13"/>
</ignoreColumnsByRegex>
</table>
8.5. <columnRenamingRule>
设置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<MbgTest> 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({
"<script> ",
"insert into mbg_test (status, ",
"version, created_time, ",
"another_status, ",
"msg, long_msg)",
"values ",
"<foreach collection='records' item='record' index='index' separator=','> ",
"(",
"#{record.status,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler}, ",
"#{record.version,jdbcType=BIGINT}, #{record.createdTime,jdbcType=TIMESTAMP}, ",
"#{record.anotherStatus,jdbcType=TINYINT,typeHandler=com.myproject.core.mybatis.typehandler.IntEnumTypeHandler}, ",
"#{record.msg,jdbcType=LONGVARCHAR}, #{record.longMsg,jdbcType=LONGVARCHAR})",
")",
"</foreach>",
"</script>"
})
int insertAll(@Param("records") List<MbgTest> 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> 然后挨个处理
一个context对应一个数据库
// 读取<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.应用<columnOverride>的设置 4.处理<generatedKey>字段相关
removeIgnoredColumns(tc, columns);
calculateExtraColumnInformation(tc, columns);
applyColumnOverrides(tc, columns);
calculateIdentityColumns(tc, columns);
// 分析表的元数据,并且得到类名,主键信息等
List<IntrospectedTable> 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)会被注入其中。