1. 程式人生 > 實用技巧 >淺談差分約束

淺談差分約束

該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址Mybatis-Spring 原始碼分析 GitHub 地址Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

MyBatis的初始化

在MyBatis初始化過程中,大致會有以下幾個步驟:

  1. 建立Configuration全域性配置物件,會往TypeAliasRegistry

    別名註冊中心新增Mybatis需要用到的相關類,並設定預設的語言驅動類為XMLLanguageDriver

  2. 載入mybatis-config.xml配置檔案、Mapper介面中的註解資訊和XML對映檔案,解析後的配置資訊會形成相應的物件並儲存到Configuration全域性配置物件中

  3. 構建DefaultSqlSessionFactory物件,通過它可以建立DefaultSqlSession物件,MyBatis中SqlSession的預設實現類

因為整個初始化過程涉及到的程式碼比較多,所以拆分成了四個模組依次對MyBatis的初始化進行分析:

  • 《MyBatis初始化(一)之載入mybatis-config.xml》
  • 《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》
  • 《MyBatis初始化(三)之SQL初始化(上)》
  • 《MyBatis初始化(四)之SQL初始化(下)》

由於在MyBatis的初始化過程中去解析Mapper介面與XML對映檔案涉及到的篇幅比較多,XML對映檔案的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解

初始化(三)之SQL初始化(上)

在前面的MyBatis初始化相關文件中已經大致講完了MyBatis初始化的整個流程,其中遺漏了一部分,就是在解析<select /> <insert /> <update /> <delete />

節點的過程中,是如何解析SQL語句,如何實現動態SQL語句,最終會生成一個org.apache.ibatis.mapping.SqlSource物件的,對於這煩瑣且易出錯的過程,我們來看看MyBatis如何實現的?

我們回顧org.apache.ibatis.builder.xml.XMLStatementBuilderparseStatementNode()解析 Statement 節點時,通過下面的方法建立對應的SqlSource物件

// 建立對應的 SqlSource 物件,儲存了該節點下 SQL 相關資訊
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

langDriver是從Configuration全域性配置物件中獲取的預設實現類,對應的也就是XMLLanguageDriver,在Configuration初始化的時候設定的

public Configuration() {
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
	languageRegistry.register(RawLanguageDriver.class);
}

主要包路徑:org.apache.ibatis.scripting、org.apache.ibatis.builder、org.apache.ibatis.mapping

主要涉及到的類:

  • org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:語言驅動介面的預設實現,建立ParameterHandler引數處理器物件和SqlSource資源物件

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:繼承 BaseBuilder 抽象類,負責將SQL指令碼(XML或者註解中定義的SQL語句)解析成SqlSource(DynamicSqlSource或者RawSqlSource)資源物件

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler:定義在XMLScriptBuilder內部的一個介面,用於處理MyBatis自定義標籤(<if /> <foreach />等),生成對應的SqlNode物件,不同的實現類處理不同的標籤

  • org.apache.ibatis.scripting.xmltags.DynamicContext:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理後的SQL語句,內部提供ContextMap物件儲存上下文的引數

  • org.apache.ibatis.scripting.xmltags.SqlNode:SQL Node介面,每個XML Node會解析成對應的SQL Node物件,通過上下文可以對動態SQL進行邏輯處理,生成需要的結果

  • org.apache.ibatis.scripting.xmltags.OgnlCache:用於處理Ognl表示式

語言驅動介面的實現類如下圖所示:

LanguageDriver

org.apache.ibatis.scripting.LanguageDriver:語言驅動介面,程式碼如下:

public interface LanguageDriver {

	/**
	 * Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
	 * 建立 ParameterHandler 物件
	 *
	 * @param mappedStatement The mapped statement that is being executed
	 * @param parameterObject The input parameter object (can be null)
	 * @param boundSql        The resulting SQL once the dynamic language has been executed.
	 * @return 引數處理器
	 * @author Frank D. Martinez [mnesarco]
	 * @see DefaultParameterHandler
	 */
	ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

	/**
	 * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file. 
	 * It is called during startup, when the mapped statement is read from a class or an xml file.
	 * 建立 SqlSource 物件,從 Mapper XML 配置的 Statement 標籤中,即 <select /> 等。
	 *
	 * @param configuration The MyBatis configuration
	 * @param script        XNode parsed from a XML file
	 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
	 * @return SQL 資源
	 */
	SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

	/**
	 * Creates an {@link SqlSource} that will hold the statement read from an annotation. 
	 * It is called during startup, when the mapped statement is read from a class or an xml file.
	 * 建立 SqlSource 物件,從方法註解配置,即 @Select 等。
	 *
	 * @param configuration The MyBatis configuration
	 * @param script        The content of the annotation
	 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
	 * @return SQL 資源
	 */
	SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

定義了三個方法:

  1. createParameterHandler:獲取 ParameterHandler 引數處理器物件

  2. createSqlSource:建立 SqlSource 物件,解析 Mapper XML 配置的 Statement 標籤中,即 <select /> <update /> <delete /> <insert />

  3. createSqlSource:建立 SqlSource 物件,從方法註解配置,即 @Select 等

XMLLanguageDriver

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:語言驅動介面的預設實現,程式碼如下:

public class XMLLanguageDriver implements LanguageDriver {

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

  /**
   * 用於解析 XML 對映檔案中的 SQL
   *
   * @param configuration The MyBatis configuration
   * @param script        XNode parsed from a XML file
   * @param parameterType input parameter type got from a mapper method or
   *                      specified in the parameterType xml attribute. Can be
   *                      null.
   * @return SQL 資源
   */
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 建立 XMLScriptBuilder 物件,執行解析
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

  /**
   * 用於解析註解中的 SQL
   *
   * @param configuration The MyBatis configuration
   * @param script        The content of the annotation
   * @param parameterType input parameter type got from a mapper method or
   *                      specified in the parameterType xml attribute. Can be
   *                      null.
   * @return SQL 資源
   */
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    // <1> 如果是 <script> 開頭,表示是在註解中使用的動態 SQL
    if (script.startsWith("<script>")) {
      // <1.1> 建立 XPathParser 物件,解析出 <script /> 節點
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      // <2.1> 變數替換
      script = PropertyParser.parse(script, configuration.getVariables());
      // <2.2> 建立 TextSqlNode 物件
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) { // <2.3.1> 如果是動態 SQL ,則建立 DynamicSqlSource 物件
        return new DynamicSqlSource(configuration, textSqlNode);
      } else { // <2.3.2> 如果非動態 SQL ,則建立 RawSqlSource 物件
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }
}

實現了LanguageDriver介面:

  1. 建立 DefaultParameterHandler 預設引數處理器並返回

  2. 解析 XML 對映檔案中的 SQL,通過建立 XMLScriptBuilder 物件,呼叫其 parseScriptNode() 方法解析

  3. 解析註解定義的 SQL

    1. 如果是 <script> 開頭,表示是在註解中使用的動態 SQL,將其轉換成 XNode 然後呼叫上述方法,不瞭解的可以看看MyBatis三種動態SQL配置方式
    2. 先將註解中定義的 SQL 中包含的變數進行轉換,然後建立對應的 SqlSource 物件

RawLanguageDriver

org.apache.ibatis.scripting.defaults.RawLanguageDriver:繼承了XMLLanguageDriver,在的基礎上增加了是否為靜態SQL語句的校驗,也就是判斷建立的 SqlSource 是否為 RawSqlSource 靜態 SQL 資源

XMLScriptBuilder

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:繼承 BaseBuilder 抽象類,負責將 SQL 指令碼(XML或者註解中定義的 SQL )解析成 SqlSource 物件

構造方法

public class XMLScriptBuilder extends BaseBuilder {
    /**
	 * 當前 SQL 的 XNode 物件
	 */
	private final XNode context;
	/**
	 * 是否為動態 SQL
	 */
	private boolean isDynamic;
	/**
	 * SQL 的 Java 入參型別
	 */
	private final Class<?> parameterType;
	/**
	 * NodeNodeHandler 的對映
	 */
	private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

	public XMLScriptBuilder(Configuration configuration, XNode context) {
		this(configuration, context, null);
	}

	public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
		super(configuration);
		this.context = context;
		this.parameterType = parameterType;
		initNodeHandlerMap();
	}

	private void initNodeHandlerMap() {
		nodeHandlerMap.put("trim", new TrimHandler());
		nodeHandlerMap.put("where", new WhereHandler());
		nodeHandlerMap.put("set", new SetHandler());
		nodeHandlerMap.put("foreach", new ForEachHandler());
		nodeHandlerMap.put("if", new IfHandler());
		nodeHandlerMap.put("choose", new ChooseHandler());
		nodeHandlerMap.put("when", new IfHandler());
		nodeHandlerMap.put("otherwise", new OtherwiseHandler());
		nodeHandlerMap.put("bind", new BindHandler());
	}
}

在建構函式中會初始化 NodeHandler 處理器,分別用於處理不同的MyBatis自定義的XML標籤,例如<if /> <where /> <foreach />等標籤

parseScriptNode方法

parseScriptNode()方法將 SQL 指令碼(XML或者註解中定義的 SQL )解析成 SqlSource 物件,程式碼如下:

public SqlSource parseScriptNode() {
    // 解析 XML 或者註解中定義的 SQL
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
        // 動態語句,使用了 ${} 也算
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
  1. 通過呼叫parseDynamicTags(XNode node)方法,將解析 SQL 成 MixedSqlNode 物件,主要是將一整個 SQL 解析成一系列的 SqlNode 物件
  2. 如果是動態SQL語句,使用了MyBatis自定義的XML標籤(<if />等)或者使用了${},則封裝成DynamicSqlSource物件
  3. 否則就是靜態SQL語句,封裝成RawSqlSource物件

parseDynamicTags方法

parseDynamicTags()將 SQL 指令碼(XML或者註解中定義的 SQL )解析成MixedSqlNode物件,程式碼如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
    // <1> 建立 SqlNode 陣列
    List<SqlNode> contents = new ArrayList<>();
    /*
     * <2> 遍歷 SQL 節點中所有子節點
     * 這裡會對該節點內的所有內容進行處理然後返回 NodeList 物件
     * 1. 文字內容會被解析成 '<#text></#text>' 節點,就算一個換行符也會解析成這個
     * 2. <![CDATA[ content ]]> 會被解析成 '<#cdata-section>content</#cdata-section>' 節點
     * 3. 其他動態<if /> <where />
     */
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        // 當前子節點
        XNode child = node.newXNode(children.item(i));
        // <2.1> 如果型別是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 時
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE // <![CDATA[ ]]>節點
                || child.getNode().getNodeType() == Node.TEXT_NODE) { // 純文字
            // <2.1.1> 獲得內容
            String data = child.getStringBody("");
            // <2.1.2> 建立 TextSqlNode 物件
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) { // <2.1.2.1> 如果是動態的 TextSqlNode 物件,也就是使用了 '${}'
                // 新增到 contents 中
                contents.add(textSqlNode);
                // 標記為動態 SQL
                isDynamic = true;
            } else { // <2.1.2.2> 如果是非動態的 TextSqlNode 物件,沒有使用 '${}'
                // <2.1.2> 建立 StaticTextSqlNode 新增到 contents 中
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 <2.2> 如果型別是 Node.ELEMENT_NODE
            // <2.2.1> 根據子節點的標籤,獲得對應的 NodeHandler 物件
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) { // 獲得不到,說明是未知的標籤,丟擲 BuilderException 異常
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            // <2.2.2> 執行 NodeHandler 處理
            handler.handleNode(child, contents);
            // <2.2.3> 標記為動態 SQL
            isDynamic = true;
        }
    }
    // <3> 建立 MixedSqlNode 物件
    return new MixedSqlNode(contents);
}

<1> 建立 SqlNode 陣列 contents,用於儲存解析 SQL 後的一些列 SqlNode 物件

<2> 獲取定義的 SQL 節點中所有子節點,返回一個 NodeList 物件,這個物件中包含了該 SQL 節點內的所有資訊,然後逐個遍歷子節點

1. 其中文字內容會被解析成`<#text></#text>`節點,就算一個換行符也會解析成這個
   	2. `<![CDATA[ ]]>` 會被解析成 `<#cdata-section></#cdata-section>` 節點
         	3. 還有其他MyBatis自定義的標籤`<if /> <where />`等等

<2.1> 如果子節點是<#text />或者<#cdata-section />型別

<2.1.1> 獲取子節點的文字內容

<2.1.2> 建立 TextSqlNode 物件

<2.1.2.1> 呼叫 TextSqlNode 的 isDynamic() 方法,點選去該進去看看就知道了,如果文字中使用了${},則標記為動態 SQL 語句,將其新增至 contents 陣列中

<2.1.2.2> 否則就是靜態文字內容,建立對應的 StaticTextSqlNode 物件,將其新增至 contents 陣列中

<2.2> 如果型別是 Node.ELEMENT_NODE 時,也就是 MyBatis 的自定義標籤

<2.2.1> 根據子節點的標籤名稱,獲得對應的 NodeHandler 物件

<2.2.2> 執行NodeHandlerhandleNode方法處理該節點,建立不通型別的 SqlNode 並新增到 contents 陣列中,如何處理的在下面講述

<2.2.3> 標記為動態 SQL 語句

<3> 最後將建立 contents 封裝成 MixedSqlNode 物件

NodeHandler

XMLScriptBuilder的內部介面,用於處理MyBatis自定義標籤,介面實現類如下圖所示:

程式碼如下:

private interface NodeHandler {
    /**
     * 處理 Node
     *
     * @param nodeToHandle   要處理的 XNode 節點
     * @param targetContents 目標的 SqlNode 陣列。實際上,被處理的 XNode 節點會建立成對應的 SqlNode 物件,新增到 targetContents 中
     */
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}

這些 NodeHandler 實現類都定義在 XMLScriptBuilder 內部,用於處理不同標籤,我們逐個來看

BindHandler

實現了NodeHandler介面,<bind />標籤的處理器,程式碼如下:

/**
 * <bind />元素允許你在 OGNL 表示式(SQL語句)以外建立一個變數,並將其繫結到當前的上下文
 */
private class BindHandler implements NodeHandler {
    public BindHandler() {
        // Prevent Synthetic Access
    }
    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析 name、value 屬性
        final String name = nodeToHandle.getStringAttribute("name");
        final String expression = nodeToHandle.getStringAttribute("value");
        // 建立 VarDeclSqlNode 物件
        final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
        targetContents.add(node);
    }
}
  1. 獲取<bind />標籤的name和value屬性

  2. 根據這些屬性建立一個 VarDeclSqlNode 物件

  3. 新增到targetContents集合中

例如這樣配置:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

TrimHandler

實現了NodeHandler介面,<trim />標籤的處理器,程式碼如下:

private class TrimHandler implements NodeHandler {
    public TrimHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // <1> 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // <2> 獲得 prefix、prefixOverrides、"suffix"、suffixOverrides 屬性
        String prefix = nodeToHandle.getStringAttribute("prefix");
        String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
        String suffix = nodeToHandle.getStringAttribute("suffix");
        String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
        // <3> 建立 TrimSqlNode 物件
        TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
        targetContents.add(trim);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<if />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件

  2. 獲得 prefixprefixOverridessuffixsuffixOverrides 屬性

  3. 根據上面獲取到的屬性建立TrimSqlNode物件

  4. 新增到targetContents集合中

WhereHandler

實現了NodeHandler介面,<where />標籤的處理器,程式碼如下:

private class WhereHandler implements NodeHandler {
    public WhereHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 建立 WhereSqlNode 物件
        WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
        targetContents.add(where);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<where />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件
  2. 建立WhereSqlNode物件,該物件繼承了TrimSqlNode,自定義字首(WHERE)和需要刪除的字首(AND、OR等)
  3. 新增到targetContents集合中

SetHandler

實現了NodeHandler介面,<set />標籤的處理器,程式碼如下:

private class SetHandler implements NodeHandler {
    public SetHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
        targetContents.add(set);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<set />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件
  2. 建立SetSqlNode物件,該物件繼承了TrimSqlNode,自定義字首(SET)和需要刪除的字首和字尾(,)
  3. 新增到targetContents集合中

ForEachHandler

實現了NodeHandler介面,<foreach />標籤的處理器,程式碼如下:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 獲得 collection、item、index、open、close、separator 屬性
        String collection = nodeToHandle.getStringAttribute("collection");
        String item = nodeToHandle.getStringAttribute("item");
        String index = nodeToHandle.getStringAttribute("index");
        String open = nodeToHandle.getStringAttribute("open");
        String close = nodeToHandle.getStringAttribute("close");
        String separator = nodeToHandle.getStringAttribute("separator");
        // 建立 ForEachSqlNode 物件
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
        targetContents.add(forEachSqlNode);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<foreach />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件
  2. 獲得 collection、item、index、open、close、separator 屬性
  3. 根據這些屬性建立ForEachSqlNode物件
  4. 新增到targetContents集合中

IfHandler

實現了NodeHandler介面,<if />標籤的處理器,程式碼如下:

private class IfHandler implements NodeHandler {
    public IfHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 獲得 test 屬性
        String test = nodeToHandle.getStringAttribute("test");
        // 建立 IfSqlNode 物件
        IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
        targetContents.add(ifSqlNode);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<if />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件
  2. 獲得 test 屬性
  3. 根據這個屬性建立IfSqlNode物件
  4. 新增到targetContents集合中

OtherwiseHandler

實現了NodeHandler介面,<otherwise />標籤的處理器,程式碼如下:

private class OtherwiseHandler implements NodeHandler {
    public OtherwiseHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 物件
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        targetContents.add(mixedSqlNode);
    }
}
  1. 繼續呼叫parseDynamicTags方法解析<otherwise />標籤內部的子標籤節點,巢狀解析,生成MixedSqlNode物件
  2. 新增到targetContents集合中,需要結合ChooseHandler使用

ChooseHandler

實現了NodeHandler介面,<choose />標籤的處理器,程式碼如下:

private class ChooseHandler implements NodeHandler {
    public ChooseHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        List<SqlNode> whenSqlNodes = new ArrayList<>();
        List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
        // 解析 `<when />` 和 `<otherwise />` 的節點們
        handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
        // 獲得 `<otherwise />` 的節點,存在多個會丟擲異常
        SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
        // 建立 ChooseSqlNode 物件
        ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
        targetContents.add(chooseSqlNode);
    }

    private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes,
            List<SqlNode> defaultSqlNodes) {
        List<XNode> children = chooseSqlNode.getChildren();
        for (XNode child : children) {
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler instanceof IfHandler) { // 處理 `<when />` 標籤的情況
                handler.handleNode(child, ifSqlNodes);
            } else if (handler instanceof OtherwiseHandler) { // 處理 `<otherwise />` 標籤的情況
                handler.handleNode(child, defaultSqlNodes);
            }
        }
    }

    private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
        SqlNode defaultSqlNode = null;
        if (defaultSqlNodes.size() == 1) {
            defaultSqlNode = defaultSqlNodes.get(0);
        } else if (defaultSqlNodes.size() > 1) {
            throw new BuilderException("Too many default (otherwise) elements in choose statement.");
        }
        return defaultSqlNode;
    }
}
  1. 先逐步處理<choose />標籤的<when /><otherwise />子標籤們,通過組合 IfHandler 和 OtherwiseHandler 兩個處理器,實現對子節點們的解析

  2. 如果存在<otherwise />子標籤,則丟擲異常

  3. 根據這些屬性建立ChooseSqlNode物件

  4. 新增到targetContents集合中

DynamicContext

org.apache.ibatis.scripting.xmltags.DynamicContext:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理後的SQL語句,內部提供ContextMap物件儲存上下文的引數

構造方法

public class DynamicContext {

  /**
   * 入參儲存在 ContextMap 中的 Key
   *
   * {@link #bindings}
   */
  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  /**
   * 資料庫編號儲存在 ContextMap 中的 Key
   *
   * {@link #bindings}
   */
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
    // <1.2> 設定 OGNL 的屬性訪問器
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

  /**
   * 上下文的引數集合,包含附加引數(通過`<bind />`標籤生成的,或者`<foreach />`標籤中的集合的元素等等)
   */
  private final ContextMap bindings;
  /**
   * 生成後的 SQL
   */
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  /**
   * 唯一編號。在 {@link org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler} 使用
   */
  private int uniqueNumber = 0;

  public DynamicContext(Configuration configuration, Object parameterObject) {
    // <1> 初始化 bindings 引數
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      // 構建入參的 MetaObject 物件
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      // 入參型別是否有對應的型別處理器
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      bindings = new ContextMap(metaObject, existsTypeHandler);
    } else {
      bindings = new ContextMap(null, false);
    }
    // <2> 新增 bindings 的預設值
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }
}
型別 屬性 說明
ContextMap bindings 上下文的引數集合,包含附加引數(通過<bind />標籤生成的,或者<foreach />標籤解析引數儲存的),以及幾個預設值
StringJoiner sqlBuilder 儲存本次解析後的SQL,每次新增字串以空格作為分隔符
int uniqueNumber 唯一編號,在ForEachHandler處理節點時需要用到,生成唯一陣列作為集合中每個元素的索引(作為字尾)
  1. 初始化bindings引數,建立 ContextMap 物件
    1. 根據入參轉換成MetaObject物件
    2. 在靜態程式碼塊中,設定OGNL的屬性訪問器,OgnlRuntime 是 OGNL 庫中的類,設定ContextMap對應的訪問器是ContextAccessor
  2. bindings中新增幾個預設值:_parameter > 入參物件,_databaseId -> 資料庫識別符號

ContextMap

DynamicContext的內部靜態類,繼承HashMap,用於儲存解析動態SQL語句時的上下文的引數集合,程式碼如下:

static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;
    /**
     * parameter 對應的 MetaObject 物件
     */
    private final MetaObject parameterMetaObject;
    /**
     * 是否有對應的型別處理器
     */
    private final boolean fallbackParameterObject;

    public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
      this.parameterMetaObject = parameterMetaObject;
      this.fallbackParameterObject = fallbackParameterObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject == null) {
        return null;
      }

      if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
        return parameterMetaObject.getOriginalObject();
      } else {
        // issue #61 do not modify the context when reading
        return parameterMetaObject.getValue(strKey);
      }
    }
}

重寫了 HashMap 的 get(Object key) 方法,增加支援對 parameterMetaObject 屬性的訪問

ContextAccessor

DynamicContext的內部靜態類,實現 ognl.PropertyAccessor 介面,上下文訪問器,程式碼如下:

static class ContextAccessor implements PropertyAccessor {
    @Override
    public Object getProperty(Map context, Object target, Object name) {
      Map map = (Map) target;

      // 優先從 ContextMap 中,獲得屬性
      Object result = map.get(name);
      if (map.containsKey(name) || result != null) {
        return result;
      }

      // <x> 如果沒有,則從 PARAMETER_OBJECT_KEY 對應的 Map 中,獲得屬性
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map) parameterObject).get(name);
      }

      return null;
    }

    @Override
    public void setProperty(Map context, Object target, Object name, Object value) {
      Map<Object, Object> map = (Map<Object, Object>) target;
      map.put(name, value);
    }

    @Override
    public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }

    @Override
    public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }
}

在DynamicContext的靜態程式碼塊中,設定OGNL的屬性訪問器,設定了ContextMap.class的屬性訪問器為ContextAccessor

這裡方法的入參中的target,就是 ContextMap 物件

  1. 在重寫的getProperty方法中,先從 ContextMap 裡面獲取屬性值(可以回過去看下ContextMap的get方法)

  2. 沒有獲取到則獲取 PARAMETER_OBJECT_KEY 屬性的值,如果是 Map 型別,則從這裡面獲取屬性值

回看 DynamicContext 的構造方法,細品一下