Mybatis是如何實(shí)現(xiàn)SQL語(yǔ)句復(fù)用功能的?
點(diǎn)擊上方藍(lán)色“程序猿DD”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!

今天,我們將分析Mybatis之sqlFragment,可以翻譯為sql片段,它的存在價(jià)值在于可復(fù)用sql片段,避免到處重復(fù)編寫(xiě)。
在工作中,往往有這樣的需求,對(duì)于同一個(gè)sql條件查詢(xún),首先需要統(tǒng)計(jì)記錄條數(shù),用以計(jì)算pageCount,然后再對(duì)結(jié)果進(jìn)行分頁(yè)查詢(xún)顯示,看下面一個(gè)例子。
<sql id="studentProperties">selectstud_id as studId, name, email, dob, phonefrom studentssql><select id="countAll" resultType="int">select count(1) from (<include refid="studentProperties">include>) tmpselect><select id="findAll" resultType="Student" parameterType="map">select * from (<include refid="studentProperties">include>) tmp limit #{offset}, #{pagesize}select>
這就是sqlFragment,它可以為select|insert|update|delete標(biāo)簽服務(wù),可以定義很多sqlFragment,然后使用include標(biāo)簽引入多個(gè)sqlFragment。在工作中,也是比較常用的一個(gè)功能,它的優(yōu)點(diǎn)很明顯,復(fù)用sql片段,它的缺點(diǎn)也很明顯,不能完整的展現(xiàn)sql邏輯,如果一個(gè)標(biāo)簽,include了四至五個(gè)sqlFragment,其可讀性就非常差了。
sqlFragment里的內(nèi)容是可以隨意寫(xiě)的,它不需要是一個(gè)完整的sql,它可以是“,phone”這么簡(jiǎn)單的文本。
1.sqlFragment的解析過(guò)程
sqlFragment存儲(chǔ)于Configuration內(nèi)部。
protected final Map<String, XNode> sqlFragments = new StrictMap("XML fragments parsed from previous mappers"); 解析sqlFragment的過(guò)程非常簡(jiǎn)單。
org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)方法部分源碼。
// 解析sqlFragmentsqlElement(context.evalNodes("/mapper/sql"));// 為select|insert|update|delete提供服務(wù)buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
sqlFragment存儲(chǔ)于Map
2.select|insert|update|delete標(biāo)簽中,解析include標(biāo)簽的過(guò)程
org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()方法源碼。
// Include Fragments before parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);// 重點(diǎn)關(guān)注的方法includeParser.applyIncludes(context.getNode());// Parse selectKey after includes and remove them.processSelectKeyNodes(id, parameterTypeClass, langDriver);// Parse the SQL (pre:and were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
注釋“pre:
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 nodeProperties fullContext;String refid = getStringAttribute(source, "refid");// replace variables in include refid valuerefid = PropertyParser.parse(refid, variablesContext);Node toInclude = findSqlFragment(refid);Properties newVariablesContext = getVariablesContext(source, variablesContext);if (!newVariablesContext.isEmpty()) {// merge contextsfullContext = new Properties();fullContext.putAll(variablesContext);fullContext.putAll(newVariablesContext);} else {// no new context - use inherited fullyfullContext = variablesContext;}// 遞歸調(diào)用applyIncludes(toInclude, fullContext);if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {toInclude = source.getOwnerDocument().importNode(toInclude, true);}// 將include節(jié)點(diǎn),替換為sqlFragment節(jié)點(diǎn)source.getParentNode().replaceChild(toInclude, source);while (toInclude.hasChildNodes()) {// 將sqlFragment的子節(jié)點(diǎn)(也就是文本節(jié)點(diǎn)),插入到sqlFragment的前面toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);}// 移除sqlFragment節(jié)點(diǎn)toInclude.getParentNode().removeChild(toInclude);} else if (source.getNodeType() == Node.ELEMENT_NODE) {NodeList children = source.getChildNodes();for (int i=0; i// 遞歸調(diào)用applyIncludes(children.item(i), variablesContext);}} else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {// replace variables in all attribute valuessource.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));} else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {// replace variables ins all text nodessource.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));}}
上面是對(duì)源碼的解讀,為了便于理解,我們接下來(lái)采用圖示的辦法,演示其過(guò)程。
3.圖示過(guò)程演示
①解析節(jié)點(diǎn)
<select id="countAll" resultType="int">select count(1) from (<include refid="studentProperties">include>) tmpselect>
②include節(jié)點(diǎn)替換為sqlFragment節(jié)點(diǎn)
<select id="countAll" resultType="int">select count(1) from ("studentProperties" >selectstud_id as studId, name, email, dob, phonefrom students) tmpselect>
③將sqlFragment的子節(jié)點(diǎn)(文本節(jié)點(diǎn))insert到sqlFragment節(jié)點(diǎn)的前面。注意,對(duì)于dom來(lái)說(shuō),文本也是一個(gè)節(jié)點(diǎn),叫TextNode。
<select id="countAll" resultType="int">select count(1) from (selectstud_id as studId, name, email, dob, phonefrom students"studentProperties" >selectstud_id as studId, name, email, dob, phonefrom students) tmpselect>
④移除sqlFragment節(jié)點(diǎn)
<select id="countAll" resultType="int">select count(1) from (selectstud_id as studId, name, email, dob, phonefrom students) tmpselect>
⑤最終結(jié)果如圖所示

(Made In QQ截圖及時(shí)編輯)
如此一來(lái),TextNode1 + TextNode2 + TextNode3,就組成了一個(gè)完整的sql。遍歷select的三個(gè)子節(jié)點(diǎn),分別取出TextNode的value,append到一起,就是最終完整的sql。
這也是為什么要移除
這就是Mybatis的sqlFragment,以上示例,均為靜態(tài)sql,即static sql。
往期推薦

我的星球是否適合你?
點(diǎn)擊閱讀原文看看我們都聊過(guò)啥?
