MyBatis源碼筆記(六) -- 解析SQL語句節點封裝爲MappedStatement

MyBatis解析XML的SQL語句節點或註解@Select等,最終會封裝成MappedStatement對象,存進configuration裏,本篇分析XML解析方式

從XMLMapperBuilder類的buildStatementFromContext方法開始

private void buildStatementFromContext(List<XNode> list) {
    //如果有指定數據庫標誌
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      //構建所有語句,一個mapper下可以有很多select
      //語句比較複雜,核心都在這裏面,所以調用XMLStatementBuilder
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
          //核心XMLStatementBuilder.parseStatementNode
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
          //如果出現SQL語句不完整,把它記下來,塞到configuration去
        configuration.addIncompleteStatement(statementParser);
      }
    }
}

上面如果有指定數據庫標誌,那會調用兩次buildStatementFromContext方法,而該方法的核心在於statementParser的parseStatementNode方法。

之所以在buildStatementFromContext方法,是因爲在parseStatementNode方法中,會調databaseIdMatchesCurrent進行判斷過濾是否進行MappedStatement構建。在指定數據庫標識的情況下,如果SQL語句節點的databaseId屬性也契合的話,優先級最高。

/**
 * @param id SQL語句節點的id屬性
 * @param databaseId 節點中databaseId屬性
 * @param requiredDatabaseId configuration中的變量;數據庫標誌
 * @return
 */
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      if (!requiredDatabaseId.equals(databaseId)) {
        return false;//不一致返回false
      }
    } else {
      if (databaseId != null) {
		//存在databaseId屬性直接返回false,因爲上面的if已經做了判斷
        return false;
      }
      // skip this statement if there is a previous one with a not null databaseId
		//保證是帶命名空間前綴
      id = builderAssistant.applyCurrentNamespace(id, false);
		//檢查是否存在相同id的MappedStatement(不包括沒完整構建出來的)
      if (this.configuration.hasStatement(id, false)) {
		//存在,那就拿到之前的
        MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
		//如果之前的存在databaseId屬性,那就返回false
		//因爲之前的肯定通過了上面和requiredDatabaseId比較的判斷
        if (previous.getDatabaseId() != null) {
          return false;
        }
      }
    }
    return true;
 }

下面開始看核心解析方法:

//解析語句(select|insert|update|delete)
//<select
//  id="selectPerson"
//  parameterType="int"
//  parameterMap="deprecated"
//  resultType="hashmap"
//  resultMap="personResultMap"
//  flushCache="false"
//  useCache="true"
//  timeout="10000"
//  fetchSize="256"
//  statementType="PREPARED"
//  resultSetType="FORWARD_ONLY">
//  SELECT * FROM PERSON WHERE ID = #{id}
//</select>
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    //如果databaseId不匹配,退出,上面已列出
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //暗示驅動程序每次批量返回的結果行數
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超時時間
    Integer timeout = context.getIntAttribute("timeout");
    //引用外部 parameterMap,已廢棄
    String parameterMap = context.getStringAttribute("parameterMap");
    //參數類型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //引用外部的 resultMap(高級功能)
    String resultMap = context.getStringAttribute("resultMap");
    //結果類型
    String resultType = context.getStringAttribute("resultType");
    //腳本語言,mybatis3.2的新功能
    String lang = context.getStringAttribute("lang");
    //得到語言驅動,默認是XML語言驅動
    LanguageDriver langDriver = getLanguageDriver(lang);
	//拿到返回類型的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    //結果集類型,是對jdbc的resultset不同處理,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一種
    String resultSetType = context.getStringAttribute("resultSetType");
    //語句類型, STATEMENT|PREPARED|CALLABLE 的一種,默認是預編譯型
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
	//mybatis把FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE封裝成了枚舉類型
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    //獲取命令類型(select|insert|update|delete)
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //如果是查詢默認不清緩存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否要緩存select結果
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //僅針對嵌套結果 select 語句適用:如果爲 true,就是假設包含了嵌套結果集或是分組了,這樣的話當返回一個主結果行的時候,就不會發生有對前面結果集的引用的情況。
    //這就使得在獲取嵌套的結果集的時候不至於導致內存不夠用。默認值:false。 
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //1.解析之前先解析<include>SQL片段,拼裝到當前的sql中,如:
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //2.解析之前先解析<selectKey>
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // 上面解析完後,會把<include>、<selectKey>節點刪掉
    //3.解析成SqlSource,一般是DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    //(僅對 insert 有用) 標記一個屬性, MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設置它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    //(僅對 insert 有用) 標記一個屬性, MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設置它的值
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
	//4.主鍵生成器賦值
    if (configuration.hasKeyGenerator(keyStatementId)) {
		//如果已存在改keyStatementId的主鍵生成器,則直接複製
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
		//如果屬性值沒有useGeneratedKeys,就根據isUseGeneratedKeys配置和是否是插入語句決定
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

	//5.去調助手類,這裏構建一個MappedStatement存入configuration
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

上面代碼有點長,前面部分是屬性獲取,然後解析<include>、<selectKey>節點並刪除,接着解析封裝一個SqlSource對象,再接着就是主鍵生成器賦值,最後構建MappedStatement。

1.解析之前先解析<include>節點

略  ̄□ ̄||

2. <selectKey> 節點解析

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    removeSelectKeyNodes(selectKeyNodes);//刪除節點
 	}

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
}

上面調了兩次parseSelectKeyNodes,套路跟文章開頭那裏的類似,真正解析在parseSelectKeyNode方法中

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    //默認是後置設置id
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    //defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = new NoKeyGenerator();
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;
	//2.1 解析節點封裝爲sqlSource對象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
	//設SQL類型爲SELECT
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
	//2.2構建一個MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
	//2.3添加一個SelectKeyGenerator
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
 }

解析流程是:解析節點封裝爲sqlSource對象,然後構建一個MappedStatement,接着在爲該添加一個SelectKeyGenerator

2.1解析節點封裝爲sqlSource對象

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
	//用XML腳本構建器解析
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
 	}

public SqlSource parseScriptNode() {
	//解析XML的SQL語言封裝成SqlNode,放進集合
    List<SqlNode> contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {//動態SQL封裝爲DynamicSqlSource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {//靜態SQL封裝爲RawSqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
 }

上面解析SQL語句時,只要sql語句有${}、或者帶有、等標籤,isDynamic標誌會設爲true。

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));
		//如果是XML節點文本內容
      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()) {//這裏主要判斷sql語句中是否有${}
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
			//不是動態SQL,封裝成StaticTextSqlNode
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
		//這裏就是動態語言節點解析,如<if>、<where>等
        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;
 }

sqlnode_impl

public interface SqlNode {
 		boolean apply(DynamicContext context);
}

SqlNode是個接口,實現類有十個,只有一個方法,該方法是對SQL語句作進一步的處理,其中StaticTextSqlNode表示不是動態SQL類型,

2.2構建一個MappedStatement

	//增加映射語句
  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    
    //爲id加上namespace前綴
    id = applyCurrentNamespace(id, false);
    //是否是select語句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    //又是建造者模式
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

    //參數映射
    setStatementParameterMap(parameterMap, parameterType, statementBuilder);
    //結果映射
    setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    //建造好調用configuration.addMappedStatement
    configuration.addMappedStatement(statement);
    return statement;
  }

MappedStatement記錄着SQL節點的詳細信息,最終存在configuration中

2.3添加一個SelectKeyGenerator

平時使用selectKey例子如下

<selectKey resultType="java.lang.Long" keyProperty="id" order="BEFORE" >
     SELECT LAST_INSERT_ID()
</selectKey>

public interface KeyGenerator {
  //定了2個回調方法,processBefore,processAfter
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

KeyGenerator會跟在SQL執行前後分別調用processBefore、processAfter,其中parameter是我們函數的入參

看看SelectKeyGenerator如何實現兩個方法

	  @Override
	  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
	    if (executeBefore) {
	      processGeneratedKeys(executor, ms, parameter);
	    }
	  }
	
	  @Override
	  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
	    if (!executeBefore) {
	      processGeneratedKeys(executor, ms, parameter);
	    }
	  }

最終都是調用processGeneratedKeys

private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
        String[] keyProperties = keyStatement.getKeyProperties();
        final Configuration configuration = ms.getConfiguration();
		//
        final MetaObject metaParam = configuration.newMetaObject(parameter);
        if (keyProperties != null) {
          //這裏創建一個SimpleExecutor
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
		  //執行語句
          List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
          if (values.size() == 0) {
            throw ...
          } else if (values.size() > 1) {
            throw ...
          } else {//結果數量爲1才正確
            MetaObject metaResult = configuration.newMetaObject(values.get(0));
            if (keyProperties.length == 1) {
              if (metaResult.hasGetter(keyProperties[0])) {
				//返回結果是個對象,判斷是否有getter方法
                setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
              } else {
                //返回結果是個基本類型, 設置id屬性值
                setValue(metaParam, keyProperties[0], values.get(0));
              }
            } else {
				//多個屬性處理
                handleMultipleProperties(keyProperties, metaParam, metaResult);
            }
          }
        }
      }
    } catch ...
 }

3.解析成SqlSource

參考2.1

4.主鍵生成器賦值

略  ̄□ ̄||

5.去調助手類,這裏構建一個MappedStatement

參考2.2

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