MyBatis源碼分析之Script用法詳解

MyBatis源碼分析之Script用法詳解

在上一篇文章中講到MyBatis的#{paras}和${paras}用法,在裏面提到在解析sql組裝成SqlSource對象時,會判斷當前sql是否是動態類型,然後裏面有一個對sql中是否含有script的判斷,這種用法我以前也沒有用過,就看到同事寫過一回,感覺和xml中的寫法差不多,就是一種動態sql,可以有一些if、else之類的條件判斷,今天就來了解一下這個用法,順便看下源碼實現。

1. Script用法


與之前相同,講到這裏還是先講一下

@ResultMap("BaseResultMap")
@Select("<script>" +
            "select * from user " +
            "<where>" +
            "<if test=\"age != null\"> age = #{age}</if>" +
            "</where>" +
            "</script>")
List<User> getUser4(@Param("age") Integer age);

判斷傳進去的age字段是否爲空,爲空則查詢全部,不爲空則查詢對應字段值下的記錄,這個動態sql其實寫的挺麻煩的,還要轉義,注意script的結束等等,我覺得還不如xml寫起來方便。

調用程序爲:

List<User> users = userMapper.getUser4(null);
System.out.println(users);
System.out.println();
List<User> users2 = userMapper.getUser4(25);
System.out.println(users2);

查詢的結果和預料中差不多,users中爲全部記錄,users2中爲1條記錄。

在這裏插入圖片描述

2. 源碼解析

上述中講述了

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

可以轉到createSqlSource方法看一看。

@Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

在parseScriptNode中進行了具體的sql類型判斷,其中如果isDynamic類型爲true,則返回DynamicSqlSource,否則返回RawSqlSource,isDynamic在parseDynamicTags方法中進行初始化,進這個方法看看。

protected MixedSqlNode 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));
      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 = 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);
  }

這個方法用來解析節點,調試一下看下。

在這裏插入圖片描述

此時node節點內容與我們之前查看的sql相同,中間分析的過程非常多,比較複雜,這就不再單獨看了,可以直接看這個方法的返回。

在這裏插入圖片描述

這裏返回的rootSQLNode中contents對象有兩個,一個爲固定查詢sql,一個爲條件sql,最後在這返回DynamicSqlSource對象。

最後我看執行到executor中的query時的代碼。

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

調試到getBoundSql處,拿到BoundSql查看,兩次分別的結果爲:

在傳入age爲null時,返回爲不帶查詢條件的sql。

在age不爲null時,此時返回的sql爲:

在這裏插入圖片描述

此時就有age查詢條件。我們進getBoundSql方法中查看具體細節。

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();
        }
      }
    }

在調試代碼時可知此時獲得的boundSql對象已經是組裝完成的sql。

在這裏插入圖片描述

此時已經將sql組裝完成了,我們繼續進SqlSource方法中看看。

@Override
  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;
  }

在調用rootSqlNode.apply方法,在前面分析知這裏的SqlNode是MixedSqlNode.

@Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }

此時的contents對象就是兩個,一個爲固定sql,一個是where條件,如下圖:

在這裏插入圖片描述

第一個sqlNode沒啥說的,固定sql調用append方就可以了,如下:

@Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

進入where條件的SQLNode,where條件沒有實現apply方法,此處調用的爲父類TrimSqlNode類的apply方法,apply方法爲:

@Override
  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
  }

在調用contents.apply(filteredDynamicContext)方法時還要再回到MixSqlNode處,此時調用就是IfSqlNode。

在這裏插入圖片描述

此時的動態sql就是where中的條件了,然後後續filteredDynamicContext.applyAll()中的操作就是給sql加上where前綴,這裏就不再分析了,有興趣的可以自己看看。


Script用法比較簡單,我原想原理應該也是和之前差不多,沒想到分析進來這麼多處理和判斷,可以說看得暈頭轉向,此處先留個記號,以後有空還要再回來仔細分析一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章