提取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: [email protected]
* @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: [email protected]
* @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 ==