引子
之前项目中一直使用的是JPA作为ORM框架,最近,新加了一个子服务,使用的是 MyBatis 作为ORM框架。既然用的是MyBatis,那就免不了 循环迭代参数、if 动态SQL查询等。
然而,MyBatis 的动态SQL要写很多判断逻辑、迭代逻辑,老是从其它SQL中去Copy就显得太Low了。所以,我查了查MyBatis的官方文档,最终找到了解决方案,这里我将方法分享出来,希望能帮到大家。
解决方案
翻阅了官方文档,我看到了下面这段话:
MyBatis 从 3.2 开始支持可插拔脚本语言,这允许你插入一种脚本语言驱动,并基于这种语言来编写动态 SQL 查询语句。
同时,我还得知:我们可以通过自己实现 LanguageDriver 接口,来自定义我们的SQL脚本解析协议。而在默认情况下,我们使用的XML格式的动态SQL,都是通过 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver 去解析的。
所以,我们可以通过重写 XmlLanguageDriver 的相关方法,来实现自定义的动态SQL。
核心代码
下面我给出了SQL解析与使用的核心代码,完整代码请参考 代码附件。
下面,我自定义了 IN 查询的 循环语法、if 查询的语法:
public class CustomXmlLanguageDriver extends XMLLanguageDriver {
/**
* 自定义 IN 查询语法规则 正则 (#{xxCollection})
*/
private static final Pattern CUSTOM_IN_QUERY_RULE = Pattern.compile("IN\\s*\\(\\s*#\\{(.*?)}\\s*\\)", Pattern.CASE_INSENSITIVE);
/**
* 自定义 if 条件查询 if(name != null and someTable.column != "" ) [a.name = #{name} and other expressions]
*/
private static final Pattern CUSTOM_IF_NULL_RULE = Pattern.compile("if\\s*\\((.*?)\\)\\s*\\[(.*?)]", Pattern.CASE_INSENSITIVE);
private static final Pattern SCRIPT_TAG_PATTERN = Pattern.compile("^<script>.*</script>$");
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
boolean scriptFlag = false;
Matcher inRuleMatcher = CUSTOM_IN_QUERY_RULE.matcher(script);
if (inRuleMatcher.find()) {
script = inRuleMatcher.replaceAll(" IN (<foreach collection='$1' item='element' separator=','> #{element} </foreach>) ");
scriptFlag = true;
}
Matcher ifRuleMatcher = CUSTOM_IF_NULL_RULE.matcher(script);
if (ifRuleMatcher.find()) {
script = ifRuleMatcher.replaceAll(" <if test = \"$1\"> $2 </if> ");
scriptFlag = true;
}
if (scriptFlag) {
// 检测SQL是否被<script>包裹
Matcher scriptTagPattern = SCRIPT_TAG_PATTERN.matcher(script);
if (!scriptTagPattern.find()) {
script = "<script>" + script + "</script>";
}
}
return super.createSqlSource(configuration, script, parameterType);
}
}
上面,我重写了 createSqlSource() 方法,在里面加入了自定义的SQL脚本解析逻辑。我是通过正则去匹配的,当然,大家可以自己实现字符串的匹配方式。这里,需要额外说明的几点是:
- Pattern.CASE_INSENSITIVE 代表的是,匹配的时候,忽略大小写。if 可以匹配到 if 和 IF。
- Mather.replaceAll()中的 $1 $2,分别等价于 matcher.group(1)、matcher.group(2) 的值。
上面我们自定义了 foreach 和 if 的动态SQL后,下面就是具体的使用了:
@Mapper
public interface UserMapper {
@Lang(CustomXmlLanguageDriver.class)
@Select({" SELECT u.id, u.account, u.role ",
" FROM user u",
" WHERE u.id IN (#{ids}) ",
" if (role != null and role != '') [AND u.role = #{role}] "})
List<User> getUserByIdAndRole(@Param("ids") Collection<Integer> ids, @Param("role") String role);
}
上面,我们通过 @Lang 注解标注该方法,表示使用我们重写的 CustomXmlLanguageDriver 类来解析下面的SQL脚本。按照我们正则定义逻辑,当我们使用 (#{ids}) 的时候,这一部分就会被替换为:
(<foreach collection='#{ids}' item='element' separator=','>
#{element}
</foreach>)
而我们使用 if (role != null and role != '') [AND u.role = #{role}] 的时候,这一部分就会被替换为:
<if test = "role != null and role != '' ">
AND u.role = #{role}
</if>
这样对比下来,我们自定义的语法的确是方便了很多。
代码附件
GitHub:https://github.com/Zereao/SpringBucket/tree/master/spring-boot-mybatis-custom-language-driver
完整代码:https://download.youkuaiyun.com/download/Zereao/12031773
参考文档
1、https://mybatis.org/mybatis-3/zh/dynamic-sql.html
2、http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/