MyBatis源码篇-动态SQL实现原理

一、SqlSource与BoundSql

1、SqlSource
  • SqlSource用于描述SQL资源,MyBatis可以通过两种方式配置SQL信息,一种是通过@Select、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider等注解;另一种是通过XML配置文件,SqlSource就是代表Java注解或者XML文件配置的SQL资源
  • 4种SqlSource实现类:
    • ProviderSqlSource:描述通过@Select、@SelectProvider等注解配置的SQL资源信息
    • DynamicSqlSource:描述Mapper XML文件中配置的SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句
    • RawSqlSource:描述Mapper XML文件中配置的SQL资源信息,这些语句在解析xml配置的时候就能确定,即不包含动态SQL相关配置
    • StaticSqlSource:用于描述ProviderSqlSource,DynamicSqlSource,RawSqlSource解析后得到的静态SQL资源
  • StaticSqlSource只封装了Mapper解析后的SQL内容和Mapper参数
2、BoundSql
  • Executor组件与数据库交互时,除了需要参数映射信息外,还需要参数信息,而StaticSqlSource只封装了Mapper解析后的SQL内容和Mapper参数映射信息
  • BoundSql是Executor组件执行SQL信息的封装,Executor通过BoundSql与数据库交互
public class BoundSql {

  // Mapper配置解析后的sql语句
  private final String sql;
  // Mapper参数映射信息
  private final List<ParameterMapping> parameterMappings;
  // Mapper参数对象
  private final Object parameterObject;
  // 额外参数信息,包括<bind>标签绑定的参数,内置参数
  private final Map<String, Object> additionalParameters;
  // 参数对象对应的MetaObject对象
  private final MetaObject metaParameters;

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }

  public String getSql() {
    return sql;
  }

  public List<ParameterMapping> getParameterMappings() {
    return parameterMappings;
  }

  public Object getParameterObject() {
    return parameterObject;
  }

  public boolean hasAdditionalParameter(String name) {
    String paramName = new PropertyTokenizer(name).getName();
    return additionalParameters.containsKey(paramName);
  }

  public void setAdditionalParameter(String name, Object value) {
    metaParameters.setValue(name, value);
  }

  public Object getAdditionalParameter(String name) {
    return metaParameters.getValue(name);
  }
}
  • BoundSql 除了封装了Mapper解析后的SQL语句和参数映射信息外,还封装了Mapper调用时传入的参数对象
  • MyBatis任意一个Mapper都有两个内置的参数,即_parameter和_databaseId。_parameter代表整个参数,包括<bind>标签绑定的参数信息,这些参数存放在BoundSql 对象的additionalParameters属性中。_databaseId为Mapper配置中通过databaseId属性指定的数据库类型

二、LanguageDriver详解

  • LanguageDriver用来实现SQL配置信息到SqlSource对象转换
  • LanguageDriver的两个实现类
    • XMLLanguageDriver为XML语言驱动,为MyBatis提供了通过XML标签结合OGNL表达式语法实现动态SQL的功能
    • RawLanguageDriver仅支持静态SQL配置,不支持动态SQL功能
public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }

  // 处理XML文件中配置的SQL信息
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 解析XML文件中配置的SQL信息
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 解析sql资源
    return builder.parseScriptNode();
  }

  // 处理Java注解中配置的SQL信息
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 解析java注解中配置的SQL信息
    // 若字符串以<script>标签开头,则以XML方式解析
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局变量
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // 如果SQL中仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

}

三、动态Sql解析过程

XMLScriptBuilder#parseScriptNode

  public SqlSource parseScriptNode() {
    // 将SQL配置转换为SqlNode对象
    // GO
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  • 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象,然后判断是否为动态SQL,动态SQL,创建DynamicSqlSource对象,否则创建RawSqlSource对象
  • MyBatis判断是否为动态的标准是SQL配置是否包含<if>、<where>、<trim>等元素或者#{}参数占位符

XMLScriptBuilder#parseDynamicTags

protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 若SQL文本中包含${}参数占位符,则为动态SQL
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        // 若SQL文本中不包含${}参数占位符,则不为动态SQL
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      // 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }
  • 对SQL配置的所有子元素进行遍历,如果子元素类型为SQL文本,则使用TextSqlNode对象描述SQL节点信息,若SQL节点存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
  • XMLScriptBuilder类定义了一个私有的NodeHandler接口,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态的SQL标签转化为对应的SqlNode对象
  • NodeHandler接口的实现类BindHandler、TrimHandler、WhereHandler、SetHandler、ForEachHandler、IfHandler、OtherwiseHandler、ChooseHandler

四、生成sql

  • 动态SQL标签解析完成后,将解析后生成的SqlNode对象封装在SqlSource对象中
  • SqlSource创建完毕后,最终会存放在MappedStatement对象的SqlSource属性中
  • Executor组件操作数据库时,会调用MappedStatement#getBoundSql()方法获取BoundSql对象

MappedStatement#getBoundSql

public final class MappedStatement {
public BoundSql getBoundSql(Object parameterObject) {
    // 完成SqlNode解析成Sql语句的过程 
    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;
  }
}

DynamicSqlSource#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
    // 通过参数对象创建动态SQL上下文对象
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 以DynamicContext对象作为参数调用apply()方法
    rootSqlNode.apply(context);
    // 创建SqlSourceBuilder对象
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 调用DynamicContext的getSql()方法获取动态SQL解析后的内容
    // 然后调用SqlSourceBuild的parse()方法对SQL内容做进一步的处理,生成StaticSqlSource对象
    // GO
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 调用StaticSqlSource对象的getBoundSql()方法获得BoundSql实例
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 将<bind>标签绑定的参数添加到BoundSql对象中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }
  • 根据参数对象创建DynamicContext 对象
  • 调用SqlNode的apply方法对动态Sql进行解析
  • context.getSql() 获取动态SQL解析后的结果
  • 调用sqlSourceParser.parse方法对动态Sql解析后的结果进一步解析处理,返回StaticSqlSource

SqlSourceBuilder#parse

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // ParameterMappingTokenHandler为MyBatis参数映射处理器,用于处理#{}参数占位符
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // GenericTokenParser用于对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }
  • ParameterMappingTokenHandler用于处理SQL中的#{}参数占位符
  • GenericTokenParser 会对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容
  • 例如参数占位符配置
    :#{userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler},
    经过GenericTokenParser 解析后,获取参数占位符内容,即
    userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler,该对象内容会经过ParameterMappingTokenHandler对象进行替换处理

GenericTokenParser#parse

 public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 获取#{在SQL中的位置
    int start = text.indexOf(openToken);
    // 不存在#{占位符
    if (start == -1) {
      return text;
    }
    // 将SQL转为char数组
    char[] src = text.toCharArray();
    // 用于记录已解析的#{或者}的偏移量,避免重复解析
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    // expression为#{}中的参数内容
    StringBuilder expression = null;
    // 遍历获取所有#{}参数占位符的内容,然后调用TokenHandler的handleToken()的方法替换参数占位符
    do {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 调用TokenHandler的handleToken()方法替换参数占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
  • 对SQL配置中的所有#{}参数占位符进行解析,获取参数占位符的内容
  • 调用ParameterMappingTokenHandler#handleToken方法对参数占位符内容进行替换

ParameterMappingTokenHandler#handleToken

   @Override
    public String handleToken(String content) {
      // GO
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }
  • 参数占位符内容被替换成了"?"字符,因为MyBatis默认情况下会使用PreparedStatement对象与数据库交互,因此#{}被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符赋值
  • buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象
  • ParameterMapping对象用于描述MyBatis参数映射消息,便于后续根据参数映射信息获取对应的TypeHandler为PreparedStatement对象设置值

SqlSourceBuilder#buildParameterMapping

  private ParameterMapping buildParameterMapping(String content) {
      // 将占位符内容转换为Map对象
      Map<String, String> propertiesMap = parseParameterMapping(content);
      // property对应的值为参数占位符名称,例如userId
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      // 如果内置参数或<bind>标签绑定的参数包含该属性,则参数类型为Getter方法返回值类型
      if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = metaParameters.getGetterType(property);
      // 判断该参数类型是否注册了TypeHandler,如果注册了,则使用参数类型
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      // 如果指定了jdbcType属性,并且为CURSOR类型,则使用ResultSet类型
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
      // 如果参数类型为Map接口的子类型,则使用Object类型
      } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
      } else {
        // 获取parameterType对应的MetaClass对象,方便获取参数类型的反射信息
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        // 如果参数类型中包含property属性指定的内容,则使用Getter方法返回类型
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          propertyType = Object.class;
        }
      }
      // 使用构建者模式构建ParameterMapping对象
      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();
        // 指定ParameterMapping对象属性
        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 " + PARAMETER_PROPERTIES);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      // 返回ParameterMapping对象
      return builder.build();
    }

五、#{}与${}区别

  • ${}参数占位符的解析是在TextSqlNode类的apply()方法中完成的

TextSqlNode#apply

  @Override
  public boolean apply(DynamicContext context) {
    // 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对此处理参数占位符内容
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // GO
    context.appendSql(parser.parse(text));
    return true;
  }
  • BindingTokenParser对参数占位符进行替换

BindingTokenParser#handleToken

 public String handleToken(String content) {
      // 获取Mybatis内置参数_parameter,_parameter属性中保存所有参数信息
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        // 将参数对象添加到ContextMap对象中
        context.getBindings().put("value", parameter);
      }
      // 通过OGNL表达式获取参数值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      // 获取参数值
      return srtValue;
    }
  • BindingTokenParser#handleToken会根据参数占位符名称获取对应的参数值,然后替换为对应的参数值
  • ${}与#{}总结
    • 使用占位符#{},占位符内容会被替换成"?"
    • 而${}参数内容占位符会直接被替换为参数值

六、总结

  • SqlSource用于描述MyBatis中的SQL资源信息;LanguageDriver用于解析SQL配置,将SQL配置信息转换为SqlSource对象;SqlNode用于描述动态SQL中<if>、<where>等标签信息
  • LanguageDriver解析配置时,会把<if>、<where>等动态SQL标签转换为SqlNode对象,封装在SqlSource中
  • 解析后的SqlSource对象会作为MappedStatement对象的属性保存在MappedStatement对象中
  • 执行Mapper时,会根据传入的参数信息调用SqlSource对象的getBoundSql()方法获取BoundSql对象,这个对象完成了将SqlNode对象转换为Sql语句的过程
  • ${}占位符会直接替换为传入的参数文本内容;#{}占位符会被替换为"?",然后调用JDBC中PreparedStatement对象的setXXX()方法为参数占位符设置值
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容