Mybatis3.4.x技術內幕(十六):Mybatis之sqlFragment(可複用的sql片斷)

Mybatis目前最新版本爲3.4.0,所以,我也將個人項目由3.3.1替換爲3.4.0。在上一篇博文中,詳細分析了Mybatis在使用foreach循環進行批量insert,返回主鍵id列表時,若是使用BatchExecutor,那麼因爲Mybatis存在bug,返回的id列表將是null值。很遺憾的告訴你們,Mybatis3.4.0依然是沒有修復該bug的,該bug依然存在。java

今天,咱們將分析Mybatis之sqlFragment,能夠翻譯爲sql片斷,它的存在價值在於可複用sql片斷,避免處處重複編寫。node

在工做中,每每有這樣的需求,對於同一個sql條件查詢,首先須要統計記錄條數,用以計算pageCount,而後再對結果進行分頁查詢顯示,看下面一個例子。sql

<sql id="studentProperties"><!--sql片斷-->
		select 
			stud_id as studId
			, name, email
			, dob
			, phone
		from students
	</sql>
	
	<select id="countAll" resultType="int">
		select count(1) from (
			<include refid="studentProperties"></include><!--複用-->
		) tmp
	</select>
	
	<select id="findAll" resultType="Student" parameterType="map">
		select * from (
			<include refid="studentProperties"></include><!--複用-->
		) tmp limit #{offset}, #{pagesize}
	</select>

這就是sqlFragment,它能夠爲select|insert|update|delete標籤服務,能夠定義不少sqlFragment,而後使用include標籤引入多個sqlFragment。在工做中,也是比較經常使用的一個功能,它的優勢很明顯,複用sql片斷,它的缺點也很明顯,不能完整的展示sql邏輯,若是一個標籤,include了四至五個sqlFragment,其可讀性就很是差了。apache

sqlFragment裏的內容是能夠隨意寫的,它不須要是一個完整的sql,它能夠是「,phone」這麼簡單的文本。網絡

1.sqlFragment的解析過程app

sqlFragment存儲於Configuration內部。dom

protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

解析sqlFragment的過程很是簡單。ui

org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)方法部分源碼。spa

// 解析sqlFragment
sqlElement(context.evalNodes("/mapper/sql"));
// 爲select|insert|update|delete提供服務
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

sqlFragment存儲於Map<String, XNode>結構當中。其實最關鍵的,是它如何爲select|insert|update|delete提供服務的。.net

2.select|insert|update|delete標籤中,解析include標籤的過程

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()方法源碼。

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 重點關注的方法
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

註釋「pre: <selectKey> and <include> were parsed and removed」,含義爲解析完,並移除。爲何要移除呢?祕密都隱藏在applyIncludes()方法內部了。

org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)方法源碼。

/**
   * Recursively apply includes through all SQL fragments.
   * @param source Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(Node source, final Properties variablesContext) {
    if (source.getNodeName().equals("include")) {
      // new full context for included SQL - contains inherited context and new variables from current include node
      Properties fullContext;

      String refid = getStringAttribute(source, "refid");
      // replace variables in include refid value
      refid = PropertyParser.parse(refid, variablesContext);
      Node toInclude = findSqlFragment(refid);
      Properties newVariablesContext = getVariablesContext(source, variablesContext);
      if (!newVariablesContext.isEmpty()) {
        // merge contexts
        fullContext = new Properties();
        fullContext.putAll(variablesContext);
        fullContext.putAll(newVariablesContext);
      } else {
        // no new context - use inherited fully
        fullContext = variablesContext;
      }
      // 遞歸調用
      applyIncludes(toInclude, fullContext);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 將include節點,替換爲sqlFragment節點
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        // 將sqlFragment的子節點(也就是文本節點),插入到sqlFragment的前面
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 移除sqlFragment節點
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      NodeList children = source.getChildNodes();
      for (int i=0; i<children.getLength(); i++) {
        // 遞歸調用
        applyIncludes(children.item(i), variablesContext);
      }
    } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
      // replace variables in all attribute values
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
      // replace variables ins all text nodes
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

上面是對源碼的解讀,爲了便於理解,咱們接下來採用圖示的辦法,演示其過程。

3.圖示過程演示

①解析節點

<select id="countAll" resultType="int">
		select count(1) from (
			<include refid="studentProperties"></include>
		) tmp
	</select>

②include節點替換爲sqlFragment節點

<select id="countAll" resultType="int">
		select count(1) from (
				<sql id="studentProperties">
					select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				</sql>
		) tmp
	</select>

③將sqlFragment的子節點(文本節點)insert到sqlFragment節點的前面。注意,對於dom來講,文本也是一個節點,叫TextNode。

<select id="countAll" resultType="int">
		select count(1) from (
				select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				<sql id="studentProperties">
					select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				</sql>
		) tmp
	</select>

④移除sqlFragment節點

<select id="countAll" resultType="int">
		select count(1) from (
				select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
		) tmp
	</select>

⑤最終結果如圖所示

(Made In QQ截圖及時編輯)

如此一來,TextNode1 + TextNode2 + TextNode3,就組成了一個完整的sql。遍歷select的三個子節點,分別取出TextNode的value,append到一塊兒,就是最終完整的sql。

這也是爲何要移除<selectKey> and <include>節點的緣由。

這就是Mybatis的sqlFragment,以上示例,均爲靜態sql,即static sql,有關動態sql,即dynamic sql,將在後續博文中進行仔細分析。

 

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索