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)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)