MyBatis源码分析四:动态Sql实现

2021-02-26 15:12:33 浏览数 (1)

一、什么是动态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执行。

0 人点赞