一、什么是动态SQL&MyBatis的支持
动态Sql即最终执行的Sql是根据输入参数确定的,最常用的场景是输入参数为一个对象,里面有3个属性,我们会做一个通用的查询方法,然后某个属性不为空则在WHERE中动态加条件。
MyBatis对于动态Sql支持如下:
1、if标签
代码语言:javascript复制<select id="getUserByEntity" resultType="com.oneplus.basex.entity.UserEntity">
select
<include refid="userFields"/>
from user
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
<if test="phone != null">
AND phone = #{phone}
</if>
</where>
</select>
上面的例子中如果输入参数中只要id, name, phone3个属性任意一个不为空就会出现在where语句中。
2、choose/when/otherwise
代码语言:javascript复制<select id="getUserByEntity" resultType="com.oneplus.basex.entity.UserEntity">
select
<include refid="userFields"/>
from user Where phone in
<choose>
<when test="id != null">
AND id = #{id}
</when>
<when test="name != null">
AND name = #{name}
</when>
<otherwise test="phone != null">
AND phone = #{phone}
</otherwise>
</select>
注意when标签每种情况是互斥的,即如果其中1个条件满足则条件则不会再检测。
3、foreach标签
一般用于对集合遍历,像IN表达式:
代码语言:javascript复制<select id="getUserByEntity" resultType="com.oneplus.basex.entity.UserEntity">
select
<include refid="userFields"/>
from user Where phone IN
<foreach item="phone" index="index" collections="phoneList"
open="(" separator="," close=")">
#{phone}
</foreach>
</select>
当然还有其它如trim/set标签,这里就一一举例了。
二、MyBatis中关于动态Sql的相关类
在讲述更多细节前,我们先举起个例子,示例XML如下:
代码语言:javascript复制<?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="com.liujh.mapper.UserMapper">
<sql id="userFields">
id,name, phone,create_time
</sql>
<select id="getUserByEntity" resultType="com.liujh.entity.UserEntity">
select
<include refid="userFields"/>
from user
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
<if test="phone != null">
AND phone = #{phone}
</if>
</where>
</select>
</mapper>
1、BoundSql
封装了Mapper解析Sql语句参数信息,还封装了Mapper调用时传入的参数对象。可以理解为这就是一个动态Sql对象的描述。
2、SqlNode
用于描述Mapper Sql配置中的Sql节点,这个是实现动态Sql非常基础的一个类。
它只有一个方法apply,用于将自己添加到最终的sql中,我们看下最简单的TextSqlNode,它代表sql中无标签的内容:
代码语言:javascript复制 public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
可以看到,因为它没有判断逻辑,在处理完后直接将自己添加到context中。
具体的实现类有:
IfSqlNode
代表一个if标签语句
代码语言:javascript复制 <if test="id != null">
AND id = #{id}
</if>
ForEachSqlNode
代表前面讲的foreach标签。
其它就不一一详述了,可参考如下:
我们可以手写一段代码替代Xml来加深理解:
代码语言:javascript复制 @Test
public void testSqlNode() {
SqlNode node1= new StaticTextSqlNode("select * from user where 1=1");
SqlNode node2= new IfSqlNode(new StaticTextSqlNode(" AND id = #{id}"),"id != null");
SqlNode node3= new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"),"name != null");
SqlNode node4= new IfSqlNode(new StaticTextSqlNode(" AND phone = #{phone}"),"phone != null");
SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(node1, node2, node3, node4));
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("id","2");
DynamicContext context = new DynamicContext(sqlSession.getConfiguration(),paramMap);
mixedSqlNode.apply(context);
System.out.println(context.getSql());
}
以下相当于Xml如下:
代码语言:javascript复制 select * from user
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
<if test="phone != null">
AND phone = #{phone}
</if>
</where>
上面会打印以下内容:
代码语言:javascript复制select * from user where 1=1 AND id = #{id}
即将各Sql节点进行判断是否有值,将无值的节点进行删除,并且接凑了Sql语句。
MixedSqlNode可以理解为SqlNode的容器,即将其它SqlNode组合在一起。
3、XMLLanguageDriver
实现了LanguageDriver接口,后者用于对Mapper Sql配置进行解析。
三、动态Sql的解析过程
从XMLLanguageDriver::createSqlSource说起
在MyBatis初始化解析Mapper Xml时会调用这个方法解析sql:
代码语言:javascript复制 public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
它会调用XMLScriptBuilder的parseScriptNode:
代码语言:javascript复制 public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
可 以看到如果是动态标签,则创建DynamicSqlSource,反之则创建RawSqlSource。
是否动态指是否包含前面介绍的if、where等标签。
代码语言:javascript复制 List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i ) {
XNode child = node.newXNode(children.item(i));
//sql文本
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" nodeName "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
这里的逻辑分2种情况:
1是sql文本,默认创建TextSqlNode,如果没有任何标签的,则创建StaticTextSqlNode,
其它情况则调用NodeHandle处理,
代码语言:javascript复制NodeHandler nodeHandlers(String nodeName) {
Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
map.put("trim", new TrimHandler());
map.put("where", new WhereHandler());
map.put("set", new SetHandler());
map.put("foreach", new ForEachHandler());
map.put("if", new IfHandler());
map.put("choose", new ChooseHandler());
map.put("when", new IfHandler());
map.put("otherwise", new OtherwiseHandler());
map.put("bind", new BindHandler());
return map.get(nodeName);
}
可以看到不同标签有不同的Handler,我们看下if标签:
代码语言:javascript复制 public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
按要求创建IfSqlNode,详细逻辑可以自己看下,不是很复杂。
再来看BoundSql是如何获取的,Executor操作DB时会调用MappedStatement获取BoundSql对象的
代码语言:javascript复制 public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
其中SqlSource对应实现有DynamicSqlSource用于处理动态SqlSource,
代码语言:javascript复制public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
先调用SqlNode的apply进行解析,然后调用SqlSourceBuilder的parse进行进一步处理。
我们看下SqlSourceBuilder的处理逻辑:
代码语言:javascript复制 public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这里主要的逻辑是处理#{}占位符,如下为示例:
代码语言:javascript复制#{id,jdbcType=INTEGER},
再来看GenericTokenParser的处理逻辑:
代码语言:javascript复制public String parse(String text) {
StringBuilder builder = new StringBuilder();
if (text != null && text.length() > 0) {
char[] src = text.toCharArray();
int offset = 0;
int start = text.indexOf(openToken, offset);
while (start > -1) {
if (start > 0 && src[start - 1] == '\') {
// the variable is escaped. remove the backslash.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start openToken.length();
} else {
int end = text.indexOf(closeToken, start);
if (end == -1) {
builder.append(src, offset, src.length - offset);
offset = src.length;
} else {
builder.append(src, offset, start - offset);
offset = start openToken.length();
String content = new String(src, offset, end - offset);
//重点逻辑
builder.append(handler.handleToken(content));
offset = end closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
}
return builder.toString();
}
这里openToken就是#{, closeToken就是}
主要逻辑是扫描openToken和closeToken的内容然后调用handler.handleToken处理。
代码语言:javascript复制 public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property != null) {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
} else {
propertyType = Object.class;
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" name "' was found in mapping #{" content "}. Valid properties are " parameterProperties);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
handleToken调用buildParameterMapping生成ParameterMapping然后加到ParameterMappings中再返回?
看明白逻辑了吗,对于每个#{},会替换成?,然后生成一个ParameterMapping加入到ParameterMappings中。
举个例子,一个Sql如下
代码语言:javascript复制 <select id="getUserByPhone" resultType="com.liujh.entity.UserEntity">
select * from user
where phone = #{phone, jdbcType=VARCHAR}
</select>
会替换成如下:
代码语言:javascript复制 select * from user where phone =?
至于为什么是?,因为MyBatis底层是依赖于JDBC,会生成PreparedStatement,后者对于参数绑定要求使用?,然后调用setXXX进行参数绑定。
四、总结
1、对于动态Sql,MyBatis用BoundSql来表示;
2、对于每个节点用SqlNode表示,像文本用TextSqlNode表示,if标签用IfSqlNode表示;
3、所有Sql节点用MixedSqlNode表示,节点有先后顺序;
4、生成Sql过程如下:调用每个SqlNode的apply将自己加到最终的大Sql中,然后解析Sql中${}部分,将这一部分内容转换成JDBC要求的格式,如将参数部分转成?,然后增加一个参数绑定;
5、最终调用JDBC的Api完成Sql执行,具体可以看以前的文档MyBatis源码分析三:Sql执行。