提取MyBatis中XML语法构造SQL的功能

提取MyBatis中XML语法构造SQL的功能

MyBatis能够使用 *.xml来编辑XML语法格式的SQL语句,常用的xml标签有<where>, <if>, <foreach>等。

偶然遇到一个场景,只想使用MyBatis的解析XML语法生成SQL的功能,而不需其他功能,于是在@Select打断点,跟踪代码执行,后续发现和XML有关的类主要在包路径org.apache.ibatis.scripting.xmltags

下面只用简单的例子举例如何仅使用MyBaits中XML生成SQL的功能,不做太多抽象/封装逻辑、不考虑 SQL注入 等安全问题,以演示功能为主

1. 数据库表定义

person表定义
create table person
(
    id     int auto_increment comment '主键' primary key,
    name   varchar(255) null comment '名称',
    gender tinyint(1)   null comment '性别, 0 female, 1 man',
    age    int          null comment '年龄, 0~200'
);

2. XML文件中SQL代码

假设有如下XML语法的SQL代码片段,这里`item`故意和`collection`重名,主要是方便后续解析
<script>
  select * from person 
      where name like CONCAT('%', #{name} ,'%') 
      <if test='ageList != null'>
          and age in 
          <foreach collection='ageList' open='(' close =')' item='ageList' separator=','> 
                #{ageList}
          </foreach>
      </if> 
      <if test='gender != null'> 
          and gender > #{gender} 
      </if>
</script>

3. 代码示例和输出

3.1 使用XML语法生成SQL (#{}的版本)

java代码 (#{}的版本)

Java代码
package com.example.springboottest;

import com.google.common.base.Splitter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2023/7/22 2:44 PM
 */
public class MyBatisSqlTest2 {
    public static void main(String[] args) {

        //1.  这里用 Map 存放查询参数(实际项目中可以用POJO类或其他形式)
        Map<String, Object> paramMap = new HashMap<>();
        List<Integer> ageList = new ArrayList<>();
        ageList.add(18);
        ageList.add(19);
        paramMap.put("ageList", ageList);
        paramMap.put("name", "person");
        paramMap.put("gender", -1);

        // 2. 打印 SQL 中使用到的查询参数
        System.out.println("==== SQL中使用到的参数: ==== start ==");
        paramMap.entrySet().forEach(System.out::println);
        System.out.println("==== SQL中使用到的参数: ==== end ==" + System.lineSeparator());

        // 3. 构造XML语法的SQL (实际项目中可以通过注解等形式封装SQL字符串)
        String anotherSql = "<script>" +
                "select * from person " +
                "<where> " +
                "name like CONCAT('%', #{name} ,'%') " +
                "<if test='ageList != null'>" +
                "and age in " +
                "<foreach collection='ageList' open='(' close =')' item='ageList' separator=','>" +
                "#{ageList}" +
                "</foreach>" +
                "</if>" +
                "<if test='gender != null'> " +
                "and gender > #{gender} " +
                "</if> " +
                "</where>"
                + "</script>";
        Configuration configuration = new Configuration();
        XMLLanguageDriver xmlLanguageDriver = new XMLLanguageDriver();
        SqlSource sqlSource = xmlLanguageDriver.createSqlSource(configuration, anotherSql, Map.class);
        BoundSql boundSql = sqlSource.getBoundSql(paramMap);
        String preparedSQL = boundSql.getSql();

        // 4. 输出 预编译SQL (?表示需要填充传入的参数的位置)
        System.out.println("==== 预编译SQL : ==== start ==");
        System.out.println(preparedSQL);
        System.out.println("==== 预编译SQL : ==== end ==" + System.lineSeparator());

        // 5. 输出 预编译SQL的 参数列表 (之后替代 ? 位置)
        System.out.println("==== 预编译SQL的参数列表 : ==== start ==");
        boundSql.getParameterMappings().forEach(System.out::println);
        System.out.println("==== 预编译SQL的参数列表 : ==== end ==" + System.lineSeparator());

        // 6. 替换预编译SQL的 ?, 传递参数 (或者XML的SQL中直接使用 ${} 则preparedSQL直接是最终的SQL, 下面这边再做替换其实也没啥意义)
        Splitter splitter = Splitter.on("?");
        Iterable<String> splitIterable = splitter.split(preparedSQL);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        StringBuilder stringBuilder = new StringBuilder();
        int i = 0;
        for (String str : splitIterable) {
            if (StringUtils.isBlank(str)) {
                continue;
            }
            stringBuilder.append(str);
            ParameterMapping paramObj = parameterMappings.get(i);
            String fieldName = paramObj.getProperty();
            Object value;
            // 这里 ITEM_PREFIX 见处理<foreach>的 org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX
            if (fieldName.startsWith(ITEM_PREFIX)) {
                fieldName = fieldName.substring(7);
                int indexIndex = fieldName.lastIndexOf('_');
                int valueIndex = NumberUtils.toInt(fieldName.substring(indexIndex + 1));
                fieldName = fieldName.substring(0, indexIndex);
                List listValue = (List) paramMap.get(fieldName);
                value = listValue.get(valueIndex);
            } else {
                value = paramMap.get(fieldName);
            }
            stringBuilder.append(value);
            i++;
        }
        String finalSqlWithParam = stringBuilder.toString();

        // 7. 输出最后传入参数后的 SQL (实际用 ${} 即可免去自己再处理一遍参数的情况)
        System.out.println("==== 最终完整的SQL : ==== start ==");
        System.out.println(finalSqlWithParam);
        System.out.println("==== 最终完整的SQL : ==== end ==");
    }
}

运行输出结果 (#{}的版本)

运行输出结果
==== SQL中使用到的参数: ==== start ==
gender=-1
name=person
ageList=[18, 19]
==== SQL中使用到的参数: ==== end ==

==== 预编译SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', ? ,'%') and age in (?,?) and gender > ?
==== 预编译SQL : ==== end ==

==== 预编译SQL的参数列表 : ==== start ==
ParameterMapping{property='name', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='__frch_ageList_0', mode=IN, javaType=class java.lang.Integer, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='__frch_ageList_1', mode=IN, javaType=class java.lang.Integer, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='gender', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
==== 预编译SQL的参数列表 : ==== end ==

==== 最终完整的SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 最终完整的SQL : ==== end ==

3.2 使用XML语法生成SQL (${}的版本)

java代码 (${}的版本)

Java代码
package com.example.springboottest;

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2023/7/22 2:44 PM
 */
public class MyBatisSqlTest2 {
    public static void main(String[] args) {

        //1.  这里用 Map 存放查询参数(实际项目中可以用POJO类或其他形式)
        Map<String, Object> paramMap = new HashMap<>();
        List<Integer> ageList = new ArrayList<>();
        ageList.add(18);
        ageList.add(19);
        paramMap.put("ageList", ageList);
        paramMap.put("name", "person");
        paramMap.put("gender", -1);

        // 2. 打印 SQL 中使用到的查询参数
        System.out.println("==== SQL中使用到的参数: ==== start ==");
        paramMap.entrySet().forEach(System.out::println);
        System.out.println("==== SQL中使用到的参数: ==== end ==" + System.lineSeparator());

        // 3. 构造XML语法的SQL (实际项目中可以通过注解等形式封装SQL字符串)
        String anotherSql = "<script>" +
                "select * from person " +
                "<where> " +
                "name like CONCAT('%', ${name} ,'%') " +
                "<if test='ageList != null'>" +
                "and age in " +
                "<foreach collection='ageList' open='(' close =')' item='ageList' separator=','>" +
                "${ageList}" +
                "</foreach>" +
                "</if>" +
                "<if test='gender != null'> " +
                "and gender > ${gender} " +
                "</if> " +
                "</where>"
                + "</script>";
        Configuration configuration = new Configuration();
        XMLLanguageDriver xmlLanguageDriver = new XMLLanguageDriver();
        SqlSource sqlSource = xmlLanguageDriver.createSqlSource(configuration, anotherSql, Map.class);
        BoundSql boundSql = sqlSource.getBoundSql(paramMap);
        String preparedSQL = boundSql.getSql();

        // 4. 输出 预编译SQL (?表示需要填充传入的参数的位置)
        System.out.println("==== 预编译SQL : ==== start ==");
        System.out.println(preparedSQL);
        System.out.println("==== 预编译SQL : ==== end ==" + System.lineSeparator());

        // 5. 输出 预编译SQL的 参数列表 (之后替代 ? 位置)
        System.out.println("==== 预编译SQL的参数列表 : ==== start ==");
        boundSql.getParameterMappings().forEach(System.out::println);
        System.out.println("==== 预编译SQL的参数列表 : ==== end ==" + System.lineSeparator());

        // 6. 替换预编译SQL的 ?, 传递参数 (或者XML的SQL中直接使用 ${} 则preparedSQL直接是最终的SQL, 下面这边再做替换其实也没啥意义)
//        Splitter splitter = Splitter.on("?");
//        Iterable<String> splitIterable = splitter.split(preparedSQL);
//        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//        StringBuilder stringBuilder = new StringBuilder();
//        int i = 0;
//        for (String str : splitIterable) {
//            if (StringUtils.isBlank(str)) {
//                continue;
//            }
//            stringBuilder.append(str);
//            ParameterMapping paramObj = parameterMappings.get(i);
//            String fieldName = paramObj.getProperty();
//            Object value;
//            // 这里 ITEM_PREFIX 见处理<foreach>的 org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX
//            if (fieldName.startsWith(ITEM_PREFIX)) {
//                fieldName = fieldName.substring(7);
//                int indexIndex = fieldName.lastIndexOf('_');
//                int valueIndex = NumberUtils.toInt(fieldName.substring(indexIndex + 1));
//                fieldName = fieldName.substring(0, indexIndex);
//                List listValue = (List) paramMap.get(fieldName);
//                value = listValue.get(valueIndex);
//            } else {
//                value = paramMap.get(fieldName);
//            }
//            stringBuilder.append(value);
//            i++;
//        }
//        String finalSqlWithParam = stringBuilder.toString();

        // 7. 输出最后传入参数后的 SQL (实际用 ${} 即可免去自己再处理一遍参数的情况)
        System.out.println("==== 最终完整的SQL : ==== start ==");
//        System.out.println(finalSqlWithParam);
        System.out.println(preparedSQL);
        System.out.println("==== 最终完整的SQL : ==== end ==");
    }
}


运行输出结果 (${}的版本)

运行输出结果
==== SQL中使用到的参数: ==== start ==
gender=-1
name=person
ageList=[18, 19]
==== SQL中使用到的参数: ==== end ==

==== 预编译SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 预编译SQL : ==== end ==

==== 预编译SQL的参数列表 : ==== start ==
==== 预编译SQL的参数列表 : ==== end ==

==== 最终完整的SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 最终完整的SQL : ==== end ==
posted on 2023-07-23 14:57  Ashiamd  阅读(251)  评论(0编辑  收藏  举报