工作之前我个人项目的ORM一直使用JPA,那时我一直认为它很酷,直到工作后手写SQL犯懵时,我才发现我是错的。

Mybatis之所以更为流行是因为学习曲线平缓,维护容易,这对互联网公司实在太重要了,互联网公司的业务调整非常频繁,因为调整业务而增加表字段是常有的事。至此,我开始放弃Hibernate/JPA,拥抱Mybatis,但使用Mybatis都有一个问题,不管在公司写业务代码还是写side project,都需要手写MybatisXML(下文简称XML),写基础的增删改查十分繁琐,当涉及到的实体类与表一多就更加痛苦,如果我一不小心在几个地方写错几个字符,后面单元测试报出的错误常常会令我怀疑自己究竟合不合适写代码。我尝试过一款大家熟知的XML生成工具,但是当XML一出来的时候我完全奔溃了,当然不是说它不好用,而且生成的方法名我一个都不喜欢,而且XML的格式我十分不喜欢, 光是看着就十分难受更何况后期的维护。

在代码洁癖欲的驱动下我只能自己动手写一款XML生成工具了。在公司里DAO类的方法应该遵循公司原有的规约,XML应该格式规范,易于维护。比如有的公司DAO的方法喜欢用find开头,有的遵循阿里规范使用get,list开头。所以工具应该可以根据各自的习惯修改方法名,而且XML内容与格式应该可以自定义。

大家熟知的XML生成工具通过JDBC连接数据库获取表字段信息,再根据字段生成实体类与XML文件,这其实有点过度设计了,而且根本没法定制,代码格式不说,如果实体类继承,比如BaseModel,生成文件修改下来实在不如自己老老实实写一份。能够遵循公司原有代码规约以及根据具体场景定制生成XML,这才是最好用的工具。

假设现在我写好了User实体类与UserDao类,看看如何生成XML吧。

User类:

1
2
3
4
5
6
7
8
9
10
public class User extends BaseModel {
private static final long serialVersionUID = 1L;
private String username;
    private String password;
    private Long loginCount;
    private Date loginTime;
    private String countryCode;
    private String phone;
    // Getter Setter
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseModel implements Serializable {
    private static final long serialVersionUID = 1L;
    public static final int DELETED = 1;
    public static final int NOT_DELETED = 0;
    protected Integer id;
    protected Date createTime;
    protected Date updateTime;
    protected Integer status;
    public BaseModel() { status = NOT_DELETED; }
// Getter Setter
}

user表

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(30) NOT NULL DEFAULT '',
`password` varchar(32) NOT NULL DEFAULT '',
`login_count` int(11) NOT NULL DEFAULT '0',
`country_code` varchar(10) NOT NULL DEFAULT '',
`phone` varchar(11) NOT NULL DEFAULT '',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
)

UserDao类:(QueryCondition是一个分页与查询辅助类)

1
2
3
4
5
6
7
8
9
public interface UserDao {
    User getById(int id);
    void insert(User user);
    int update(User user);
    int deleteById(int id);
    List<User> listByIds(@Param("ids") List<Integer> ids);
    List<User> list(QueryCondition<User> query);
    int count(User user);
}

UserDao列出的方法是一些基本的增删改查方法,几乎是所有DAO类都有的方法。

我希望最后生成的XML像这么漂亮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<select id="getById" resultType="top.leeys.codegenerator.User">
  select * from user where id = #{id} and status = 0;
</select>
<delete id="deleteById">
  delete from user where id = #{id};
</delete>
<insert id="insert" parameterType="top.leeys.codegenerator.User" useGeneratedKeys="true" keyProperty="id">
    insert into user
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="username != null">username,</if>
        <if test="password != null">password,</if>
        <if test="loginCount != null">login_count,</if>
        <if test="loginTime != null">login_time,</if>
        <if test="countryCode != null">country_code,</if>
        <if test="phone != null">phone,</if>
        create_time,
        update_time,
        status
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="username != null">#{username},</if>
        <if test="password != null">#{password},</if>
        <if test="loginCount != null">#{loginCount},</if>
        <if test="loginTime != null">#{loginTime},</if>
        <if test="countryCode != null">#{countryCode},</if>
        <if test="phone != null">#{phone},</if>
        now(),
        now(),
        0
    </trim>
</insert>
<update id="update" parameterType="top.leeys.codegenerator.User">
    update user
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="password != null">password = #{password},</if>
        <if test="loginCount != null">login_count = #{loginCount},</if>
        <if test="loginTime != null">login_time = #{loginTime},</if>
        <if test="countryCode != null">country_code = #{countryCode},</if>
        <if test="phone != null">phone = #{phone},</if>
        update_time = now()
    </set>
    where id = #{id} and status = 0;
</update>
<select id="listByIds" resultType="top.leeys.codegenerator.User">
    <if test="ids != null and !ids.isEmpty()">
        select * from user where id in 
        <foreach collection="ids" item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
        and status = 0
    </if>
</select>
  
<select id="list" resultType="top.leeys.codegenerator.User" >
    select * from user
    <where>
        <if test="model.id != null">and id = #{model.id}</if>
        <if test="model.username != null">and username = #{model.username}</if>
        <if test="model.password != null">and password = #{model.password}</if>
        <if test="model.loginCount != null">and login_count = #{model.loginCount}</if>
        <if test="model.loginTime != null">and login_time = #{model.loginTime}</if>
        <if test="model.countryCode != null">and country_code = #{model.countryCode}</if>
        <if test="model.phone != null">and phone = #{model.phone}</if>
        and status = 0
    </where>
    <foreach item="sort" index="col" collection="orderBy"
             open="ORDER BY" separator="," close="">
        ${col} ${sort}
    </foreach>
    limit #{startRow}, #{pageSize};
</select>
<select id="count" resultType="int">
    select count(*) from user
    <where>
        <if test="id != null">and id = #{id}</if>
        <if test="username != null">and username = #{username}</if>
        <if test="password != null">and password = #{password}</if>
        <if test="loginCount != null">and login_count = #{loginCount}</if>
        <if test="loginTime != null">and login_time = #{loginTime}</if>
        <if test="countryCode != null">and country_code = #{countryCode}</if>
        <if test="phone != null">and phone = #{phone}</if>
        and status = 0
    </where>
</select>

这样看起来简洁整齐得太多了!

这里面其实有很多重复的部分,不同的XML区别只有表名,实体类的完全限定名,实体类的驼峰与下划线属性名。

似乎只要写一个模板,把变的数据填充到模板渲染就能生成XML内容了,是的,我是这么实现的。Java的模板引擎有不少,这里我使用了apache的velocity模板引擎。

现在创建XmlData类用来定义填充到velocity模板引擎的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class XmlData {
    /** 表名 */
    public String tableName;
    /** dao类完全限定名称 */
    public String daoName;
    /** 实体类完全限定名称 */
    public String entityName;
    /** 逻辑删除字段名 status or deleted */
    public String deleteFieldName = "status";
    /** 字段元组 */
    public final List<Tuple<String, String>> fields = new LinkedList<>();
// Getter
}

其中Tuple是一个元组,左边存类属性的驼峰名称,右边存下划线名称,也可以用LinkedHashMap代替(保持有序)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class Tuple<L, R> {
    private final L left;
    private final R right;
    
    public Tuple(L left, R right) {
        this.left = left;
        this.right = right;
    }
    
    public L getLeft() {
        return left;
    }
    public R getRight() {
        return right;
    }
}

现在暂时不关心XmlData对象怎么生成,假设数据存在。在src/main/resources目录创建mybatis-xml.vm的模板文件,在这个文件里你可以随心所欲编写自己喜欢的格式模板,我写的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?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="$data.daoName">
    <select id="getById" resultType="$data.entityName">
        select * from $data.tableName where id = #{id} and $data.deleteFieldName = 0;
    </select>
    
    <delete id="deleteById">
        delete from $data.tableName where id = #{id};
    </delete>
    
    <insert id="insert" parameterType="$data.entityName" useGeneratedKeys="true" keyProperty="id">
        insert into $data.tableName
        <trim prefix="(" suffix=")" suffixOverrides=",">
#foreach($field in $data.fields)
            <if test="$field.left != null">${field.right},</if>
#end
            create_time,
            update_time,
            status
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
#foreach($field in $data.fields)
            <if test="$field.left != null">#{$field.left},</if>
#end
            now(),
            now(),
            0
        </trim>
    </insert>
    
    <update id="update" parameterType="$data.entityName">
        update $data.tableName
        <set>
#foreach($field in $data.fields)
            <if test="$field.left != null">$field.right = #{$field.left},</if>
#end
             update_time = now()
        </set>
        where id = #{id} and $data.deleteFieldName = 0;
    </update>
    
    <select id="listByIds" resultType="$data.entityName">
        <if test="ids != null and !ids.isEmpty()">
            select * from $data.tableName where id in 
            <foreach collection="ids" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
            and $data.deleteFieldName = 0
        </if>
    </select>
      
    <sql id="query_filter">
        <if test="model.id != null">and id = #{model.id}</if>
#foreach($field in $data.fields)
        <if test="model.$field.left != null">and $field.right = #{model.$field.left}</if>
#end
        and $data.deleteFieldName = 0
    </sql>
    
    <sql id="count_filter">
        <if test="id != null">and id = #{id}</if>
#foreach($field in $data.fields)
        <if test="$field.left != null">and $field.right = #{$field.left}</if>
#end
        and $data.deleteFieldName = 0
    </sql>
    
    <select id="list" resultType="$data.entityName" >
        select * from $data.tableName
        <where>
            <include refid="query_filter"/>
        </where>
        <foreach item="sort" index="col" collection="orderBy"
                 open="ORDER BY" separator="," close="">
            ${col} ${sort}
        </foreach>
        limit #{startRow}, #{pageSize};
    </select>
    <select id="count" resultType="int">
        select count(*) from $data.tableName
        <where>
            <include refid="count_filter"/>
        </where>
    </select>
  
</mapper>

这里使用两个<sql></sql>节点,把查询条件独立出来是为了能够复用查询条件,使用<include refid=""/>引用。

剩下的工作就是生成DataXML对象。需要的数据如下:

  • tableName 表名
  • daoName dao类完全限定名称
  • entityName 实体类完全限定名称
  • fields 字段驼峰与蛇形元组

除了表名,剩下的使用反射即可获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class MybatisXmlGenerator {
private static VelocityEngine engine = new VelocityEngine();
    
    static {
        engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        engine.init();
    }
/**
     * 
     * @param templateName vm模板文件名
     * @param tableName 表名
     * @param DaoClz Dao类
     * @param entityClz 实体类
     * @param ignoreFields 忽略的实体字段
     * @return
     */
    public static String generateXml(String templateName, String tableName, Class<?> DaoClz, Class<?> entityClz, String... ignoreFields) {
        Set<String> ignoresSet = new HashSet<>();
        Collections.addAll(ignoresSet, ignoreFields);
        
        XmlData data = new XmlData();
        data.tableName = tableName;
        data.entityName = entityClz.getCanonicalName();
        data.daoName = DaoClz.getCanonicalName();
// Field[] fields = FieldUtils.getAllFields(entityClz); // 获取包含父类的属性
        Field[] fields = entityClz.getDeclaredFields();
        for (Field field : fields) {
            if (Modifier.isStatic(field.getModifiers()) || ignoresSet.contains(field.getName()))
                continue;
            data.fields.add(new Tuple<>(field.getName(), getSnakeCase(field.getName())));
        }
        
        VelocityContext ctx = new VelocityContext();
        ctx.put("data", data);
        StringWriter writer = new StringWriter();
        Template template = engine.getTemplate(templateName, "UTF-8");
        template.merge(ctx, writer);
        return writer.toString();
    }
/** 驼峰转下划线 */
    public static String getSnakeCase(String camelCase) {
        if (camelCase == null)
            return null;
        StringBuilder sb = new StringBuilder();
        Pattern pattern = Pattern.compile("[A-Z]?[a-z]*[^A-Z]");
        Matcher matcher = pattern.matcher(camelCase);
        while (matcher.find()) {
            sb.append(matcher.group().toLowerCase()).append("_");
        }
        return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : "";
    }
public static void main(String[] args) {
        String xml = null;
        xml = generateXml("mybatis-xml.vm", "user", UserDao.class, User.class);
        System.out.println(xml);
    }
}

总结一下使用流程:

  1. 写实体类
  2. 写DAO
  3. 生成XML

如何在项目里使用?

对于maven多模块项目,将代码放在能够引用到实体类与DAO类的地方,或者新建maven module在pom.xml引用实体类与DAO模块即可。

文章最后,如果你也不想写DAO类的CRUD方法,那么可以尝试使用模板生成,发挥你的想象力,把一切枯燥的东西用模板去生成吧。

Gihub完整代码:https://github.com/Dog-Lee/mybatis-xml-generator