【Mybatis学习】增强型注解简化SQL
2017-12-19 12:42
302 查看
1. 背景
MyBatis提供了简单的Java注解,使得我们可以不配置XML格式的Mapper文件,也能方便的编写简单的数据库操作代码:public interface UserMapper { @Select("SELECT * FROM users WHERE id = #{userId}") User getUser(@Param("userId") String userId); }
但是注解对动态SQL的支持一直差强人意,即使MyBatis提供了InsertProvider等*Provider注解来支持注解的Dynamic SQL,也没有降低SQL的编写难度,甚至比XML格式的SQL语句更难编写和维护。
注解的优势在于能清晰明了的看见接口所使用的SQL语句,抛弃了繁琐的XML编程方式。但没有良好的动态SQL支持,往往就会导致所编写的DAO层中的接口冗余,所编写的SQL语句很长,易读性差……
Mybatis在3.2版本之后,提供了LanguageDriver接口,我们可以使用该接口自定义SQL的解析方式。故在这里向大家介绍下以此来实现注解方式下的动态SQL。
2. 实现方案
我们先来看下LanguageDriver接口中的3个方法:public interface LanguageDriver { ParameterHandler createParameterHandler(MappedStatement var1, Object var2, BoundSql var3); SqlSource createSqlSource(Configuration var1, XNode var2, Class<?> var3); SqlSource createSqlSource(Configuration var1, String var2, Class<?> var3); }
createParameterHandler方法为创建一个ParameterHandler对象,用于将实际参数赋值到JDBC语句中
将XML中读入的语句解析并返回一个sqlSource对象
将注解中读入的语句解析并返回一个sqlSource对象
一旦实现了LanguageDriver,我们即可指定该实现类作为SQL的解析器,在XML中我们可以使用 lang 属性来进行指定
<typeAliases> <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/> </typeAliases> <select id="selectBlog" lang="myLanguage"> SELECT * FROM BLOG </select>
public interface Mapper { @Lang(MyLanguageDriver.class) @Select("SELECT * FROM users") List<User> selectUser(); }LanguageDriver的默认实现类为XMLLanguageDriver和RawLanguageDriver;分别为XML和Raw,Mybatis默认是XML语言,所以我们来看看XMLLanguageDriver中是怎么实现的:
public class XMLLanguageDriver implements LanguageDriver { public XMLLanguageDriver() { } public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); } public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { if(script.startsWith("<script>")) { XPathParser textSqlNode1 = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver()); return this.createSqlSource(configuration, textSqlNode1.evalNode("/script"), parameterType); } else { script = PropertyParser.parse(script, configuration.getVariables()); TextSqlNode textSqlNode = new TextSqlNode(script); return (SqlSource)(textSqlNode.isDynamic()?new DynamicSqlSource(configuration, textSqlNode) :new RawSqlSource(configuration, script, parameterType)); } } }
发现其实mybatis已经帮忙写好了解析逻辑,而且发现如果是以<Script>开头的字符串传入后,会被以XML的格式进行解析。那么方案就可以确认了,我们继承XMLLanguageDriver这个类,并且重写其createSqlSource方法,按照自己编写逻辑解析好sql后,再调用父类的方法即可。
3. 实现自定义注解
本段中给出一些常见的自定义注解的实现和使用方式。
3.1 自定义Select In注解
在使用Mybatis注解的时候,发现其对Select In格式的查询支持非常不友好,在字符串中输入<foreach>十分繁琐,可以通过将自定义的标签转成<foreach>格式;下面便通过我们自己实现的LanguageDriver来实现SQL的动态解析:DAO接口层中代码如下:
@Select("SELECT * FROM users WHERE id IN (#{userIdList})") @Lang(SimpleSelectInLangDriver.class) List<User> selectUsersByUserId(@Param("userIdList") List<Integer> userIdList);
LanguageDriver实现类如下:
// 一次编写即可 public class SimpleSelectInLangDriver extends XMLLanguageDriver implements LanguageDriver { private static final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)"); @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { Matcher matcher = inPattern.matcher(script); if (matcher.find()) { script = matcher.replaceAll("<foreach collection=\"$1\" item=\"_item\" open=\"(\" " + "separator=\",\" close=\")\" >#{_item}</foreach>"); } script = "<script>" + script + "</script>"; return super.createSqlSource(configuration, script, parameterType); } }
通过自己实现LanguageDriver,在服务器启动的时候,就会将我们自定义的标签解析为动态SQL语句,其等同于:
@Select("SELECT * " + "FROM users " + "WHERE id IN " + "<foreach item='item' index='index' collection='list' open='(' separator=',' close=')'>" + "#{item}" + "</foreach>") List<User> selectUsersByUserId(List<Integer> userIdList);
通过实现LanguageDriver,剥离了冗长的动态SQL语句,简化了Select In的注解代码。
需要注意的是在使用Select In的时候,请务必在传入的参数前加@Param注解,否则会导致Mybatic找不到参数而抛出异常。
3.2 自定义Update Bean注解
在扩展update注解时,数据库每张表的字段和实体类的字段必须遵循一个约定(数据库中采用下划线命名法,实体类中采用驼峰命名法)。当我们update的时候,会根据每个字段的映射关系,写出如下代码:<update id="updateUsersById" parameterType="com.lucifer.bean.User"> UPDATE users <set> <if test=“userName != null"> user_name = #{userName} , </if> <if test=“password != null"> password = #{password} , </if> <if test=“phone != null"> phone = #{phone}, </if> <if test=“email != null"> email = #{email}, </if> <if test=“address != null"> address = #{address}, </if> <if test="gmtCreated != null"> gmt_created = #{gmtCreated}, </if> <if test="gmtModified != null"> gmt_modified = #{gmtModified}, </if> </set> WHERE id = #{id} </update>
我们可以将实体类中的驼峰式代码转换为下划线式命名方式,这样就可以将这种映射规律自动化
经过实现LanguageDriver后,注解代码为
@Update("UPDATE users (#{user}) WHERE id = #{id}") @Lang(SimpleUpdateLangDriver.class) void updateUsersById(User user);
相对于原始的代码量有很大的减少,并且,一个类中字段越多,改善也就越明显。实现方式为:
public class SimpleUpdateLangDriver extends XMLLanguageDriver implements LanguageDriver{ private static final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)"); @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { Matcher matcher = inPattern.matcher(script); if (matcher.find()) { StringBuilder sb = new StringBuilder(); sb.append("<set>"); for (Field field : parameterType.getDeclaredFields()) { String tmp = "<if test=\"_field != null\">_column=#{_field},</if>"; sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column", CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()))); } sb.deleteCharAt(sb.lastIndexOf(",")); sb.append("</set>"); script = matcher.replaceAll(sb.toString()); script = "<script>" + script + "</script>"; } return super.createSqlSource(configuration, script, parameterType); } }
3.3 自定义Insert Bean注解
同理,我们可以抽象化Insert操作,简化后的Insert注解为@Insert("INSERT INTO users (#{user})") @Lang(SimpleInsertLangDriver.class) void insertUserDAO(User user);
实现方式为:
public class SimpleInsertLangDriver extends XMLLanguageDriver implements LanguageDriver { private static final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)"); @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { Matcher matcher = inPattern.matcher(script); if (matcher.find()) { StringBuilder sb = new StringBuilder(); StringBuilder tmp = new StringBuilder(); sb.append("("); for (Field field : parameterType.getDeclaredFields()) { sb.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()) + ","); tmp.append("#{" + field.getName() + "},"); } sb.deleteCharAt(sb.lastIndexOf(",")); tmp.deleteCharAt(tmp.lastIndexOf(",")); sb.append(") values (" + tmp.toString() + ")"); script = matcher.replaceAll(sb.toString()); script = "<script>" + script + "</script>"; } return super.createSqlSource(configuration, script, parameterType); } }
3.4 自定义Select注解
有的业务场景下,我们需要根据对象中的字段进行查询,就会写出如下代码:
<select id="selectUser" resultType="com.lucifer.bean.User"> SELECT id,user_name,password,phone,address,email FROM users <where> <if test="isDel != null"> AND is_del = #{isDel} </if> <if test="userName != null"> AND user_name = #{userName} </if> <if test="email != null"> AND email = #{email} </if> <if test="phone != null"> AND phone = #{phone} </if> </where> </select>
和Update操作一样,我们可以实现LanguageDriver将where子句抽象化,以此来简化Select查询语句。简化后代码如下:
和Update操作一样,我们可以实现LanguageDriver将where子句抽象化,以此来简化Select查询语句。简化后代码如下:
4.排除多余的变量
一个常见的情况是,可能会遇到实体类中的部分字段在数据库中并不存在相应的列,这就需要对多余的不匹配的字段进行逻辑隐藏;我们增加一个自定义的注解,并且对Language的实现稍作修改即可。注解为:
然后在实现类中将被该注解声明过的字段排除
for (Field field : parameterType.getDeclaredFields()) { if (!field.isAnnotationPresent(Invisible.class)) { // 排除被Invisble修饰的变量 String tmp = "<if test=\"_field != null\">_column=#{_field},</if>"; sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column", CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()))); } }
for (Field field : parameterType.getDeclaredFields()) { if (!field.isAnnotationPresent(Invisible.class)) { // 排除被Invisble修饰的变量 String tmp = "<if test=\"_field != null\">_column=#{_field},</if>"; sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column", CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()))); } }
5.注意事项&遇到的一些坑
务必确保数据库中列名和实体类中字段能一一对应在使用自定义SQL解析器的时候,只能传入一个参数,即相应的对象参数即可;传入多个参数会导致解析器中获得到的class对象改变,使得sql解析异常
Update的实现能满足大部分的业务,但有些业务场景可以会遇到根据查询条件来更新查询参数的情况,比如Update uesrs SET uesr_name = 'tom' WHERE user_name = 'Jack'; 在这中场景的时候请不要使用自定义的SQL解析器
请使用Mybatis 3.3以上版本。3.2版本有bug,会另开一篇重新描述
6.总结
通过实现Language Driver,我们可以方便的自定义自己的注解。在遵循一些约定的情况下(数据库下划线命名,实体驼峰命名),我们可以大幅度的减少SQL的编写量,并且可以完全的屏蔽掉麻烦的XML编写方式,再也不用编写复杂的动态SQL了有木有。相关文章推荐
- Mybatis增强型注解简化SQL语句
- Mybatis增强型注解简化SQL语句
- SpringMVC + Spring + MyBatis 学习笔记:为MyBatis增加打印SQL功能 (最简化配置)
- mybatis和spring整合的几种方式及使用注解简化sql(不使用xml方式写sql)
- 模仿与学习MyBatis - 1.6 注解与Session:sql语句
- MyBatis学习笔记-注解SQL多个参数查询异常处理
- Java Persistence with MyBatis 3(中文版) 第四章 使用注解配置SQL映射器
- MyBatis学习 之 二、SQL语句映射文件(1)resultMap
- mybatis学习笔记(二)-- 使用mybatisUtil工具类体验基于xml和注解实现
- Mybatis的selectKey——xml映射和注解映射sql
- MyBatis学习 之 三、动态SQL语句
- MyBatis基础学习:动态SQL和SQL语句构建器类
- mybatis学习四之输入输出映射以及动态sql
- mybatis学习入门使用注解增删改查(二)
- Mybatis注解-注解方式的动态SQL语句
- MyBatis学习 之 三、动态SQL语句
- mybatis 映射sql常用注解
- MyBatis学习总结(23)——Mybatis打印Sql语句配置
- Mybatis注解学习--xxxMapper is not known to the MapperRegistry
- Spring boot 和 mybatis 学习笔记3--动态sql