背景最近在做一個類似于綜合報表之類的東西,需要查詢所有的記錄(數(shù)據(jù)庫記錄有限制),大概有1W條記錄,該報表需要三個表的數(shù)據(jù),也就是根據(jù)這 1W 個 ID 去執(zhí)行查詢?nèi)螖?shù)據(jù)庫,其中,有一條查詢 SQL 是自己寫,其他兩條是根據(jù)別人提供的接口進(jìn)行查詢,剛開始的時候,沒有多想,直接使用 in 進(jìn)行查詢,使用 Mybatis 的 foreach 語句;項目中使用的是 jsonrpc 來請求數(shù)據(jù),在測試的時候,發(fā)現(xiàn)老是請求不到數(shù)據(jù),日志拋出的是 jsonrpc 超時異常,繼續(xù)查看日志發(fā)現(xiàn),是被阻塞在上面的三條SQL查詢中。 在以前分析 Mybatis 的源碼的時候,了解到,Mybatis 的 foreach 會有性能問題,所以改了下 SQL,直接在代碼中拼接SQL,然后在 Mybatis 中直接使用 # 來獲取,替換?class 測試了下,果然一下子就能查詢出數(shù)據(jù)。 前提這里先不考慮使用 in 好不好,如何去優(yōu)化 in,如何使用?exists 或 inner join 進(jìn)行代替等,這里就只是考慮使用了 in 語句,且使用了 Mybatis 的 foreach 語句進(jìn)行優(yōu)化,其實 foreach 的優(yōu)化很簡單,就是把 in 后面的語句在代碼里面拼接好,在配置文件中直接通過 #{xxx} 或 ${xxx} 當(dāng)作字符串直接使用即可。 測試在分析 foreach 源碼之前,先構(gòu)造個數(shù)據(jù)來看看它們的區(qū)別有多大。 建表語句: CREATE?TABLE?person( ????id?int(11)?PRIMARY?KEY?NOT?NULL, ????name?varchar(50), ????age?int(11), ????job?varchar(50)); 插入 1W 條數(shù)據(jù): POJO 類: @Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublic?class?Person?implements?Serializable?{????private?int?id;????private?String?name;????private?String?job;????private?int?age; } ?方式一通過原始的方式,使用 foreach 語句: 1. 在 dao 里面定義方法: List<Person>?queryPersonByIds(@Param("ids")?List<Integer>?ids); 2. 配置文件SQL: <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap"> select?*?from?person?where?1=1 <if?test="ids?!=?null?and?ids.size()?>?0"> and?id?in <foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")"> #{item} </foreach> </if></select> 3. 執(zhí)行 main 方法: @RunWith(SpringJUnit4Cla***unner.class)@ContextConfiguration(locations?=?{?"classpath:spring-mybatis.xml"?})public?class?MainTest?{????@Autowired ????private?IPersonService?personService;????@Test ????public?void?test(){????????//?構(gòu)造?1W?個?ID ????????List<Integer>?ids?=?new?ArrayList<>();????????for?(int?i?=?1;?i?<=?10000;?i )?{ ????????????ids.add(i); ????????}????????long?start?=?System.currentTimeMillis();???????? ????????//?執(zhí)行三次 ????????personService.queryPersonByIds(ids); ????????personService.queryPersonByIds(ids); ????????personService.queryPersonByIds(ids);????????long?end?=?System.currentTimeMillis(); ????????System.out.println(String.format("耗時:%d",?end?-?start)); ????} } 結(jié)果:耗時:2853 可以看到通過 foreach 的方法,大概需要 3s 方式二在代碼中封裝 SQL ,在配置文件中 通過 ${xxx} 來獲?。?/p> 1. 在 dao 添加方法: List<Person>?queryPersonByIds2(@Param("ids")?String?ids); 2. 配置文件SQL: <select?id="queryPersonByIds2"?parameterType="String"?resultMap="queryPersonMap"> select?*?from?person?where?1=1 <if?test="ids?!=?null?and?ids?!=?''"> ??and?id?in?${ids} </if></select> 3. 執(zhí)行 main 方法: @Testpublic?void?test_3(){ //?拼接?SQL? StringBuffer?sb?=?new?StringBuffer(); sb.append("("); for?(int?i?=?1;?i?<?10000;?i )?{ sb.append(i).append(","); } sb.deleteCharAt(sb.toString().length()?-?1); sb.append(")");????//?最終的?SQL?為?(1,2,3,4,5...) long?start2?=?System.currentTimeMillis();????//?執(zhí)行三次 personService.queryPersonByIds2(sb.toString()); personService.queryPersonByIds2(sb.toString()); personService.queryPersonByIds2(sb.toString()); long?end2?=?System.currentTimeMillis(); System.out.println(String.format("耗時:%d",?end2?-?start2)); } 結(jié)果:耗時:360 通過拼接 SQL,使用 ${xxx} 的方式,執(zhí)行同樣的 SQL ,耗時大概 360 ms 方式三在代碼中封裝 SQL ,在配置文件中 通過 #{xxx} 來獲?。?/p> 1. 在 dao 中添加方法: List<Person>?queryPersonByIds3(@Param("ids")?String?ids); 2. 配置文件SQL: <select?id="queryPersonByIds3"?parameterType="String"?resultMap="queryPersonMap"> select?*?from?person?where?1=1 <if?test="ids?!=?null?and?ids?!=?''"> and?id?in?(#{ids}) </if></select> 3. 執(zhí)行 main 方法: @Testpublic?void?test_3(){????//?拼接?SQL StringBuffer?sb2?=?new?StringBuffer(); for?(int?i?=?1;?i?<?10000;?i )?{ sb2.append(i).append(","); } sb2.deleteCharAt(sb2.toString().length()?-?1);????//?最終的SQL為?1,2,3,4,5.... long?start3?=?System.currentTimeMillis(); personService.queryPersonByIds3(sb2.toString()); personService.queryPersonByIds3(sb2.toString()); personService.queryPersonByIds3(sb2.toString()); long?end3?=?System.currentTimeMillis(); System.out.println(String.format("耗時:%d",?end3?-?start3)); } 結(jié)果:耗時:30 通過拼接 SQL,使用 #{xxx} 的方式,執(zhí)行同樣的 SQL ,耗時大概 30 ms 總結(jié)通過上面三種方式可以看到,使用不同的方式,耗時的差別還是麻大的,最快的是 拼接 SQL,使用 #{xxx} 當(dāng)作字符串處理,最慢的是 foreach。為什么 foreach 會慢那么多呢,后面再分析源碼的時候再進(jìn)行分析;而這里同樣是拼接 SQL 的方式,#{xxx} 和 ${xxx} 耗時卻相差 10 倍左右;我們知道,Mybatis 在解析 # 和 $ 這兩種不同的符號時,采用不同的處理策略;使用過 JDBC 的都知道,通過 JDBC 執(zhí)行 SQL 有兩種方式:?Statment 對象和PreparedStatment 對象, ?PreparedStatment 表示預(yù)編譯的SQL,包含的SQL已經(jīng)預(yù)編譯過了,SQL 中的參數(shù)部分使用 ?進(jìn)行占位,之后使用 setXXX 進(jìn)行賦值,當(dāng)使用 Statement 對象時,每次執(zhí)行一個SQL命令時,都會對它進(jìn)行解析和編譯。所有?PreparedStatment 效率要高一些。那么 Mybatis 在解析 # 和 $ 的時候,分別對應(yīng)的是這兩種對象,# 被解析成?PreparedStatment 對象,通過 ? 進(jìn)行占位,之后再賦值,而 $ 被解析成?Statement ,通過直接拼接SQL的方式賦值,所以,為什么同樣是通過在代碼中拼接 SQL ,# 和 $ 的耗時不同的原因。 PS:上面只是介紹了三種方式,應(yīng)該沒有人問,拼接SQL為 (1,2,3,4,5),在配置SQL中通過 #{xxx} 來獲取吧 foreach 源碼解析?下面來看下 foreach 是如何被解析的,最終解析的 SQL 是什么樣的: 在 Mybatis 中,foreach 屬于動態(tài)標(biāo)簽的一種,也是最智能的其中一種,Mybatis 每個動態(tài)標(biāo)簽都有對應(yīng)的類來進(jìn)行解析,而 foreach 主要是由?ForEachSqlNode?負(fù)責(zé)解析。 ForeachSqlNode 主要是用來解析 <foreach> 節(jié)點的,先來看看 <foreach> 節(jié)點的用法: <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap"> select?*?from?person?where?1=1 <if?test="ids?!=?null?and?ids.size()?>?0"> and?id?in <foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")"> #{item} </foreach> </if></select> 最終被 數(shù)據(jù)庫執(zhí)行的 SQL 為?select? * from person where 1=1 and id in (1,2,3,4,5) 先來看看它的兩個內(nèi)部類: PrefixedContext該類主要是用來處理前綴,比如 "(" 等。 private?class?PrefixedContext?extends?DynamicContext?{??? ???private?DynamicContext?delegate;????//?指定的前綴 ????private?String?prefix;????//?是否處理過前綴 ????private?boolean?prefixApplied;????//?....... ????@Override ????public?void?appendSql(String?sql)?{??????//?如果還沒有處理前綴,則添加前綴 ??????if?(!prefixApplied?&&?sql?!=?null?&&?sql.trim().length()?>?0)?{ ????????delegate.appendSql(prefix); ????????prefixApplied?=?true; ??????}???????//?拼接SQL ??????delegate.appendSql(sql); ????} } FilteredDynamicContextFilteredDynamicContext 是用來處理 #{} 占位符的,但是并未綁定參數(shù),只是把 #{item} 轉(zhuǎn)換為 #{_frch_item_1} 之類的占位符。 ??private?static?class?FilteredDynamicContext?extends?DynamicContext?{????private?DynamicContext?delegate;????//對應(yīng)集合項在集合的索引位置 ????private?int?index;????//?item的索引 ????private?String?itemIndex;????//?item的值 ????private?String?item;????//............. ????//?解析?#{item} ????@Override ????public?void?appendSql(String?sql)?{ ??????GenericTokenParser?parser?=?new?GenericTokenParser("#{",?"}",?new?TokenHandler()?{????????@Override ????????public?String?handleToken(String?content)?{??????????//?把?#{itm}?轉(zhuǎn)換為?#{__frch_item_1}?之類的 ??????????String?newContent?=?content.replaceFirst("^\\s*"? ?item? ?"(?![^.,:\\s])",?itemizeItem(item,?index));???????????//?把?#{itmIndex}?轉(zhuǎn)換為?#{__frch_itemIndex_1}?之類的 ??????????if?(itemIndex?!=?null?&&?newContent.equals(content))?{ ????????????newContent?=?content.replaceFirst("^\\s*"? ?itemIndex? ?"(?![^.,:\\s])",?itemizeItem(itemIndex,?index)); ??????????}??????????//?再返回?#{__frch_item_1}?或?#{__frch_itemIndex_1} ??????????return?new?StringBuilder("#{").append(newContent).append("}").toString(); ????????} ??????});??????//?拼接SQL ??????delegate.appendSql(parser.parse(sql)); ????}??private?static?String?itemizeItem(String?item,?int?i)?{????return?new?StringBuilder("__frch_").append(item).append("_").append(i).toString(); ??} } ForeachSqlNode?了解了 ForeachSqlNode? 它的兩個內(nèi)部類之后,再來看看它的實現(xiàn): public?class?ForEachSqlNode?implements?SqlNode?{??public?static?final?String?ITEM_PREFIX?=?"__frch_";??//?判斷循環(huán)的終止條件 ??private?ExpressionEvaluator?evaluator;??//?循環(huán)的集合 ??private?String?collectionExpression;??//?子節(jié)點 ??private?SqlNode?contents;??//?開始字符 ??private?String?open;??//?結(jié)束字符 ??private?String?close;??//?分隔符 ??private?String?separator;??//?本次循環(huán)的元素,如果集合為?map,則index?為key,item為value ??private?String?item;??//?本次循環(huán)的次數(shù) ??private?String?index;??private?Configuration?configuration;??//?............... ??@Override ??public?boolean?apply(DynamicContext?context)?{????//?獲取參數(shù) ????Map<String,?Object>?bindings?=?context.getBindings();????final?Iterable<?>?iterable?=?evaluator.evaluateIterable(collectionExpression,?bindings);????if?(!iterable.iterator().hasNext())?{??????return?true; ????}????boolean?first?=?true;????//?添加開始字符串 ????applyOpen(context);????int?i?=?0;????for?(Object?o?:?iterable)?{ ??????DynamicContext?oldContext?=?context;??????if?(first)?{????????//?如果是集合的第一項,則前綴prefix為空字符串 ????????context?=?new?PrefixedContext(context,?""); ??????}?else?if?(separator?!=?null)?{????????//?如果分隔符不為空,則指定分隔符 ????????context?=?new?PrefixedContext(context,?separator); ??????}?else?{??????????//?不指定分隔符,在默認(rèn)為空 ??????????context?=?new?PrefixedContext(context,?""); ??????}??????int?uniqueNumber?=?context.getUniqueNumber();?? ??????if?(o?instanceof?Map.Entry)?{????????//?如果集合是map類型,則將集合中的key和value添加到bindings參數(shù)集合中保存 ????????Map.Entry<Object,?Object>?mapEntry?=?(Map.Entry<Object,?Object>)?o;????????//?所以循環(huán)的集合為map類型,則index為key,item為value,就是在這里設(shè)置的 ????????applyIndex(context,?mapEntry.getKey(),?uniqueNumber); ????????applyItem(context,?mapEntry.getValue(),?uniqueNumber); ??????}?else?{????????//?不是map類型,則將集合中元素的索引和元素添加到?bindings集合中 ????????applyIndex(context,?i,?uniqueNumber); ????????applyItem(context,?o,?uniqueNumber); ??????}??????//?調(diào)用?FilteredDynamicContext?的apply方法進(jìn)行處理 ??????contents.apply(new?FilteredDynamicContext(configuration,?context,?index,?item,?uniqueNumber));??????if?(first)?{ ????????first?=?!((PrefixedContext)?context).isPrefixApplied(); ??????} ??????context?=?oldContext; ??????i ; ????}?????//?添加結(jié)束字符串 ????applyClose(context);????return?true; ??}??private?void?applyIndex(DynamicContext?context,?Object?o,?int?i)?{????if?(index?!=?null)?{ ??????context.bind(index,?o);?//?key為idnex,value為集合元素 ??????context.bind(itemizeItem(index,?i),?o);?//?為index添加前綴和后綴形成新的key ????} ??}??private?void?applyItem(DynamicContext?context,?Object?o,?int?i)?{????if?(item?!=?null)?{ ??????context.bind(item,?o); ??????context.bind(itemizeItem(item,?i),?o); ????} ??} } 所以該例子: <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap"> select?*?from?person?where?1=1 <if?test="ids?!=?null?and?ids.size()?>?0"> and?id?in <foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")"> #{item} </foreach> </if></select> 解析之后的 SQL 為: select? *? from? person where? 1=1 and id?in (#{__frch_item_0},? #{__frch_item_1}, #{__frch_item_2},?#{__frch_item_3},?#{__frch_item_4}) 之后在通過?PreparedStatment 的 setXXX 來進(jìn)行賦值。 所以,到這里,知道了 Mybatis?在解析 foreach 的時候,最后還是解析成了?#?的方式,但是為什么還是很慢呢,這是因為需要循環(huán)解析?#{__frch_item_0}?之類的占位符,foreach 的集合越大,解析越慢。既然知道了需要解析占位符,為何不自己拼接呢,所以就可以在代碼中拼接好,而不再使用 foreach 啦。 所以,Mybatis 在解析 foreach 的時候,底層還是會解析成?#?號的形式而不是?$?的形式,既然知道了這個,如果 需要 foreach 的集合很大,就可以使用代碼拼接 SQL ,使用?(#{xxx})?的方式進(jìn)行獲取,不要再拼接成?(1,2,3,4,5)?再使用??${xxx}?的方式啦。 |
|