前言
最近团队统一了技术栈,Mybatis 正是其中一项,为了加强自己的开发体验,我再度花时间研究了一下 Mybatis Generator 并开发了几款插件。
主要目标还是为了将有限的时间投入到高创造性的产出活动中(说的再高大上也是为了少写代码 ing),以及减少低级错误的发生。
本文就是对这几款插件的一个介绍,插件源码已经放在 github 上了
关于我是如何集成 generator 的,可以在 github 上查看另一个演示项目
MapperPlusPlugin
当前问题
很多时候生成的代码并不能完全满足业务需求,需要对其进行一定的扩展,扩展的最基本原则就是不修改由 Mybatis generator 生成的文件,因为这样才能在项目迭代中持续的使用 Mybatis generator,降低因为修改冲突而带来的维护成本。
为了实现扩展,一般会选择继承由 generator 生成的 Java mapper 接口,然后再手写一个 XML,这个手写的 XML 可以通过 namespace + id 的形式引用生成的 XML 中的 、 等元素
<mapper namespace="cc.cc1234.dao.mapper.UserMapper">
<select id="selectByUsername" resultMap="generated.UserGeneratedMapper.BaseResultMap">
select *
from user
where username = #{username}
</select>
</mapper>
但这种引用方式不是很便捷,如果能直接通过 id 引用而不需要加 namepace 前缀是最好的了。
<mapper namespace="cc.cc1234.dao.mapper.UserMapper">
<!-- 期望直接通过 ID 应用 -->
<select id="selectByUsername" resultMap="BaseResultMap">
select *
from user
where username = #{username}
</select>
</mapper>
其实 generator 生成的 Java Mapper 的方法都差不多(只是参数、返回值类型不同),完全可以将这些方法通过泛型设计放到一个通用的 BaseMapper 接口中去,那么扩展的时候就不是继承各自的 XxxMapper,而都是同一个 BaseMapper 了。
public class UserMapper extends BaseMapper<User, UserExample> {}
public class AddressMapper extends BaseMapper<Address, AddressExample> {}
另一个问题就是 Mybatis generator 默认生成的 Example 查询方法缺少一个常用的 selectOneByExample(Example)
方法,虽然 selectByExample
也能取单个结果,但肯定没有直接调用 selectOneByExample
香。
使用方式
<plugin type="cc.cc1234.mybatis.generator.MapperPlusPlugin">
<!-- BaseMapper 配置目录-->
<property name="base-mapper.target.project" value="${mybatis.generator.javaProjectDir}"/>
<property name="base-mapper.target.package" value="${mybatis.generator.mapper.package}"/>
<!-- BaseMapper 的名称 -->
<property name="base-mapper.name" value="OneMapper"/>
<!-- 每个 Table 对应的 mapper 配置目录 -->
<property name="java-mapper.target.project" value="src/main/java"/>
<property name="java-mapper.target.package" value="${mybatis.generator.mapper.package}"/>
<!-- 是否禁用 selectOneByExample,默认为 false -->
<property name="select-one-by-example.disabled" value="false"/>
</plugin>
插件作用
- 会在指定目录生成一个通用的 BaseMapper
public interface OneMapper<T, E> {
long countByExample(E example);
int deleteByExample(E example);
int deleteByPrimaryKey(Long id);
int insert(T row);
int insertSelective(T row);
Optional<T> selectOneByExample(E example);
T selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("row") T row, @Param("example") E example);
int updateByExample(@Param("row") T row, @Param("example") E example);
int updateByPrimaryKeySelective(T row);
}
- 重命名所有生成的 xxxMapper.xml 文件为 xxxGeneratedMapper.xml
- 会为 Mapper 生成一个 selectOneByExample 方法
public interface OneMapper<T, E> {
/* ... */
List<T> selectByExample(E example);
/* ... */
}
每个表对应的 mapper.xml 也会生成对应的 SQL
<select id="selectOneByExample" parameterType="cc.cc1234.dao.model.UserExample" resultMap="BaseResultMap">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
-->
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from user
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
limit 1
</select>
- 会在指定目录生成表对应的 Java Mapper,该 Mapper 会自动继承 BaseMapper 接口,生成的 XML 对应的 namespace 也指向了该 Java Mapper
import cc.cc1234.dao.model.Address;
import cc.cc1234.dao.model.AddressExample;
public interface AddressMapper extends OneMapper<Address, AddressExample> {
}
用户可以在该 Mapper 中添加方法,插件不会覆盖已存在的 Java Mapper,并且所有的 XML 的 namespace 都是指向的该 Java 接口(包括 generator 生成的 xml),这就代表在用户自定义的 XML 中可以直接通过 id 引用 geneartor 生成的 xml 中的内容,比如 , 等。
ExampleModelPlusPlugin
当前问题
- Example 需要通过 new 来构建,不够便捷
- 无法链式的构造多个查询条件
AddressExample example = new AddressExample();
example.createCriteria()
.andCityIsNotNull();
// NOTE: or 条件不能链式的构造,需要使用 example 单独构造 criteria
example.or()
.andStreetIsNull();
addressMapper.selectByExample(example);
- 排序条件无法像查询条件一样通过方法名称构建
AddressExample example = new AddressExample();
example.createCriteria()
.andCityIsNotNull();
// NOTE: 排序条件需要写真实的列名称,无法像查询条件一样通过调用方法构造
example.setOrderByClause("create_at desc");
List<Address> addresses = addressMapper.selectByExample(example);
为了优化 Example 的体验,ExampleModelPlusPlugin 针对以上问题做了优化,不仅使得 Example 的查询构造可以链式到底,还提供了类型安全的查询条件构造方法。
使用方式
<plugin type="cc.cc1234.mybatis.generator.ExampleModelPlusPlugin">
<!-- 禁用 order by 增强,默认 false -->
<property name="example.order-by.disabled" value="false"/>
<!-- 禁用生成 Example 的静态工厂方法,默认 false -->
<property name="example.static-factory.disabled" value="false"/>
<!-- 禁用生成 Criteria 的 example() 方法生成,默认 false -->
<property name="criteria.example.disabled" value="false"/>
</plugin>
插件作用
- 为 Example 生成一个便捷的静态工厂方法
create()
// 使用示例
XxxExample example = XxxExample.create();
- 为 Criteria 创建一个
example()
方法,可以直接返回当前 Example 类型
// 使用示例
XxxExample example = XxxExample.create()
.createCriteria()
.andUsernameLike("root%")
.andCreateAtLessThan(LocalDateTime.now())
.example(); // 返回当前 Example 实例
- 为 Example 创建一个
orderBy()
方法,该方法返回新增的 OrderByCriteria 类型,使得字段排序也可以如同查询条件一样通过方法构建
// 使用示例
UserExample example = UserExample.create()
.createCriteria()
.andUsernameLike("root%")
.andCreateAtLessThan(LocalDateTime.now())
.example()
.orderBy() // 获取一个 OrderByCriteria 类
.idDesc() // 该类为所有字段都生成了排序方法
.createAtDesc()
.example(); // 获取当前 Example 的实例
// select * from user where username like 'root%' and create_at <= now() order by id desc, create_at desc
userMapper.selectByExample(example);
NullsafePlugin
当前问题
默认情况下 mybatis generator 生成的 model 会丢失一个很重要的信息: model 中某个属性对应的列字段是否为空。
为了避免 NullPointerException,开发只好
- 对于不确定的字段都用 if 判空再做处理
- 或者就是去查看一下当前数据库的原始 schema
第一种造成了过度的防御式编程,第二种有上下文的切换烦恼。
那能不能直接从生成的 Java model 中就能得知某字段可能为空呢?这里我用 NullsafePlugin 给出了确定的答案:可以。
使用方式
<plugin type="cc.cc1234.mybatis.generator.NullSafePlugin">
<!-- 会忽略对指定列的处理 -->
<property name="ignore.columns"
value="*.create_time,*.update_time,*.create_at,*.active_from,*.active_to,*.operation_time,point_rule.start_time,point_rule.end_time,*.event_properties,*.reward"/>
<!-- 是否生成返回值为 Optional 的 get 方法 -->
<property name="optional.getter" value="true"/>
<!-- 是否添加 spring 的 @Nullable 注解 -->
<property name="spring.nullable" value="true"/>
</plugin>
插件作用
如果表中的某一列可为空
- 那么生成的 Java model 中对应字段属性会被加上
@Nullable
注解
import org.springframework.lang;Nullable;
public class Model {
@Nullable
private String xxx;
}
Nullable
注解可以让开发集成工具(IDE)或静态检查工具(比如 findbugs、sonarQuebe)帮你检查出可能造成 NPE (NullPointerException)的代码,未来插件会支持更多符合 JSR-305 标准的注解。
- 该字段会额外生成一个 getXxxOptional 的方法
public Type getXxx() {
return this.xxx;
}
/**
* 返回类型为 Optional<T>
*/
public Optional<Type> getXxxOptional() {
return Optional.ofNullable(this.xxx);
}
为什么不直接将 getXxx 方法的返回值替代为 Optional 呢?因为 mybatis 会根据 get 方法的返回值去匹配对应的 TypeHandler,如果没有一个通用的 OptionalTypeHandler 的话就出现映射异常。