前不久的項目時間緊張,為了盡快完成原型開發,寫了一段效率相當低的代碼。
最近幾天閑下來,主動把之前的代碼優化了一下:)
標簽:Java、Mybatis、MySQL
概況:本地系統從另外一個系統得到實體類集合List<UserEvent>,但是實體中只有eventId信息,其他屬性值均為空。
需要從數據庫中查詢數據,完善List<UserEvent>的信息並返回。
相關業務表以及對應的實體類,如下圖。(為了回避項目信息,相關業務內容均省略,以下表名、實體名、代碼變量名等均用字母ABC代替。)

原處理
1.先來看代碼,乍一看邏輯清晰,符合正常思維習慣。
但是仔細查看發現,for循環中的每步處理都和數據查詢有關。假設有10次循環,每次循環中有4次數據庫連接查詢,一共需要連接數據庫40次。
每次數據庫連接都需要一定的開銷,隨着循環量不斷增加,處理時間將成倍增長,造成資源浪費。
1 String eventId = ""; 2 String aTime = ""; 3 for (UserEvent event : userEventList) { 4 eventId = event.getEventId(); 5 6 // 獲取業務B信息 7 List<EntityB> listB = mapperB.selectBInfoByEventId(eventId); 8 event.setListB(listB); 9 10 // 獲取業務C信息 11 List<EntityC> listC = mapperC.selectCInfoByEventId(eventId); 12 event.setListC(listC); 13 14 // 查看是否有業務B處理 15 EntityB entityB = mapperB.selectBInfoByPrimary(phone, eventId); 16 event.setIsActionB(null == entityB ? "false" : "true"); 17 18 // 獲取業務A和用戶信息 19 User userInfo = mapperA.selectEventUserInfoByEventId(eventId); 20 if(null != userInfo){ 21 aTime = userInfo.getTime(); 22 event.setTime(aTime == null ? "" : sdfMd.format(sdfYmd.parse(aTime))); 23 event.setUserInfo(userInfo); 24 } 25 }
2.再來看查詢語句。
業務表A和業務表B沒有復雜的查詢。只有業務表C使用了一個子查詢,來獲取表內自身數據引用的信息。
各業務表數據都需要關聯到用戶表User。
1 <select id="selectBInfoByEventId" parameterType="String" resultType="EntityA"> 2 SELECT 3 B.phone AS phone, 4 B.time AS time, 5 U.name AS userName 6 FROM table_b B 7 LEFT JOIN user U ON U.phone = B.phone 8 WHERE B.event_id = #{eventId} 9 ORDER BY B.time ASC 10 </select> 11 <select id="selectCInfoByEventId" parameterType="String" resultType="EntityC"> 12 SELECT 13 C.cmtId, 14 C.referId, 15 C.time, 16 U.name AS userName 17 ( SELECT TU.name FROM table_c TC 18 LEFT JOIN user TU ON TU.phone = TC.phone 19 WHERE TC.cmt_id = TC.refer_id 20 ) AS referName 21 FROM table_c C 22 LEFT JOIN user U ON C.phone = U.phone 23 WHERE C.event_id = #{eventId} 24 ORDER BY C.time ASC 25 <select id="selectEventUserInfoByEventId" parameterType="java.lang.String" resultType="User"> 26 SELECT 27 U.name, 28 U.picId, 29 A.time 30 FROM table_a A 31 LEFT JOIN user U ON U.phone = A.phone 32 WHERE A.event_id = #{eventId} 33 </select>
優化分析
- 在代碼結構上,要避免在for循環中作查詢處理。考慮將查詢參數evenId從for循環中提取出來,做批量查詢,然后再將查詢結果設定到對應的實體類中。
- 在業務上,對於每一個UserEvent中的eventId,業務表A中必定對應有一條記錄,而在業務表B和業務表C中則未必有與這個eventId關聯的數據。因此,可以將業務表A作為主表,通過eventId與另外幾個表關聯查詢。查詢次數也由原來的至少四次減少為一次查詢。
- 對於聯合查詢的結果,以UserEvent作為查詢結果的實體類,使用Mybatis中的collection、association來處理結果映射。
- 另外,各業務表的查詢中都有與用戶表User的關聯,考慮將各業務信息的查詢處理創建為視圖。這樣不僅能簡化聯合查詢中SQL語句,也可以隔離基礎表的數據。
優化后的代碼
int eventSize = userEventList.size(); List<String> eventIds = new ArrayList<String>(); // 如果考慮去掉重復數據,可以使用集合Set,但是作為Mybatis的輸入參數,最后還是需要將Set轉化為List。
// 此處直接使用List,因為在業務上排除了重復數據的可能性。 for (int i = 0; i < eventSize; i++) { eventIds.add(userEventList.get(i).getEventId()); } Map<String, Object> paramsMap = new HashMap<String, Object>(); paramsMap.put("eventIds", eventIds); paramsMap.put("phone", phone); List<UserEvent> eventInfoList = eventMapper.selectUserEventInfo(paramsMap); // 將查詢結果轉化為Map存儲,方便調用 Map<String, UserEvent> eventInfoMap = new HashMap<String, UserEvent>(); for(UserEvent event : eventInfoList){ eventInfoMap.put(event.getEventId(), event); } UserEvent newEvent = null; String aTime = null; for(UserEvent event : roadEventList){ // 從查詢結果Map中取出補充信息,保存到原UserEvent對象中 newEvent =eventInfoMap.get(event.getEventId()); if(null != newEvent ){ aTime = newEvent.getTime(); event.setTime(aTime == null ? "" : sdfMd.format(sdfYmd.parse(aTime ))); event.setIsActionB(newEvent.getIsActionB() == null ? "false" : newEvent.getIsActionB()); event.setUserInfo(newEvent.getUserInfo()); event.setListB(newEvent.getListB()); event.setListC(newEvent.getListC()); } }
<resultMap id="UserMap" type="User"> <result column="name" property="name" /> <result column="picId" property="picId" /> <result column="time" property="time" /> </resultMap> <resultMap id="BMap" type="EntityB"> <id column="bPhone" property="phone" /> <result column="bUserName" property="userName" /> <result column="bTime" property="time" /> </resultMap> <resultMap id="CMap" type="EntityC"> <id column="cmtId" property="cmtId" /> <result column="referId" property="referId" /> <result column="cUserName" property="userName" /> <result column="referName" property="referName" /> <result column="cTime" property="time" /> </resultMap> <resultMap id="EventResultMap" type="com.xxxx.bean.UserEvent"> <id column="eventId" property="eventId" /> <result column="time" property="time" /> <result column="isActionB" property="isActionB" /> <association property="userInfo" resultMap="UserMap" /> <collection property="listB" resultMap="BMap" /> <collection property="listC" resultMap="CMap" /> </resultMap> <select id="selectUserEventInfo" resultMap="EventResultMap"> SELECT A.eventId, A.time, A.name, A.picId, CASE WHEN B.phone = #{phone} THEN "true" ELSE "false" END AS isActionB, B.phone AS bPhone, B.userName AS bUserName, B.time AS bTime, C.cmtId, C.referId, C.userName AS cUserName, C.referName AS referName, C.time AS cTime FROM v_table_a A LEFT JOIN v_table_b B ON B.eventId = A.eventId LEFT JOIN v_table_c C ON C.eventId = A.eventId <where> A.event_id in <foreach collection="eventIds" index="" item="eventId"
open="(" separator="," close=")"> #{eventId} </foreach> </where>; </select>
編碼時需要注意的幾個地方
1. 復雜對象的映射解析
采用resultMap嵌套。其中,collection標簽表示映射一個集合,association標簽表示映射一個實體類,
標簽中的property屬性值對應的是,該集合/實體在查詢結果對象中的變量名。
對於各表中名稱相同的字段,需要建立別名,否則解析時無法確定各屬性與表字段的對應關系。
如:業務表B和業務表C中都有userName字段,在查詢語句中為為字段別名加了前綴來區分。
B.userName AS bUserName,
<
result
column
=
"bUserName"
property
=
"userName"
/>
C.userName AS cUserName,
<
result
column
=
"cUserName"
property
=
"userName"
/>
resultMap中type屬性表示標簽所包含內容對應映射的Java類。
該屬性可以寫類的全路徑(如:
<
resultMap
id
=
"EventResultMap"
type
=
"com.xxxx.bean.UserEvent"
> ),
也可以配置為簡寫的類名(如:
<
resultMap
id
=
"UserMap"
type
=
"User"
> )。
簡寫的類名需要在xml配置文件中設置(如下),配好之后的簡寫類名可以在各個sql.xml中使用。
<!-- spring-mybatis.xml文件 --> <!-- 配置sqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" />
<!-- 將各Java類的簡寫別名單獨放到文件mybatis.xml中,方便修改和管理 --> <property name="configLocation" value="classpath:xml/mybatis.xml" />
<property name="mapperLocations" value="classpath:sql/*.xml" /> </bean>
<!-- mybatis.xml文件 --> <configuration> <typeAliases> <typeAlias alias="EntityA" type="com.xxxx.model.EntityA" /> <typeAlias alias="EntityB" type="com.xxxx.model.EntityB" /> <typeAlias alias="EntityC" type="com.xxxx.model.EntityC" /> <typeAlias alias="User" type="com.xxxx.model.User" /> </typeAliases> </configuration>
2. foreach標簽的使用
如果查詢接口只有一個參數,參數類型為list,則標簽中的collection屬性應該設定
collection
=
"list";參數類型為數組,則應設定為
collection
=
"array"。
如果查詢接口有多個參數,則最好通過Map來傳遞各參數。此時,foreach標簽的collection屬性應設置為,Map中表示集合參數的鍵。
如上面的代碼中,表示集合參數是
eventIds,它在Map中的鍵為
"eventIds" ,所以
collection
=
"eventIds"。
處理時間對比
各表數據量在200、300條左右,List<UserEvent>集合記錄為13條。
雖然優化后的代碼行數有所增加,查詢結果解析略微復雜,但是十幾條數據的查詢已有2秒的差距。
