拼多多一面:Mybatis是如何實現SQL語句複用功能的?
今天,我們將分析Mybatis之sqlFragment,可以翻譯為sql片段,它的存在價值在於可複用sql片段,避免到處重複編寫。
在工作中,往往有這樣的需求,對於同一個sql條件查詢,首先需要統計記錄條數,用以計算pageCount,然後再對結果進行分頁查詢顯示,看下面一個例子。
<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,其可讀性就非常差了。
sqlFragment裡的內容是可以隨意寫的,它不需要是一個完整的sql,它可以是“,phone”這麼簡單的文字。
1. sqlFragment的解析過程
sqlFragment儲存於Configuration內部。
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
解析sqlFragment的過程非常簡單。
org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)方法部分原始碼。
// 解析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提供服務的。
需要更多大廠面試資料的話也可以暗號:CSDN
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: and 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。
這也是為什麼要移除 and 節點的原因。
這就是Mybatis的sqlFragment,以上示例,均為靜態sql,即static sql。
讀者福利
感謝你看到了這裡!
我這邊整理很多2020最新Java面試題(含答案)和Java學習筆記,如下圖
上述的面試題答案小編都整理成文件筆記。 同時也還整理了一些面試資料&最新2020收集的一些大廠的面試真題(都整理成文件,小部分截圖)免費分享給大家,有需要的可以 免費分享~
如果喜歡本篇文章,歡迎轉發、點贊。
記得關注我!