一、多資料來源問題
主要思路是把dataSource、sqlSesstionFactory、MapperScannerConfigurer在配置中區分開,各Mapper對應的包名、類名區分開
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" 4 xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" 5 xmlns:context="http://www.springframework.org/schema/context" 6 xsi:schemaLocation=" 7 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd 8 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 9 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd 10 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd 11 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" 12 default-autowire="byName"> 13 14 <bean id="dataSource1" class="org.h2.jdbcx.JdbcConnectionPool" 15 destroy-method="dispose"> 16 <constructor-arg> 17 <bean class="org.h2.jdbcx.JdbcDataSource"> 18 <property name="URL" value="jdbc:h2:r:/h2db/awbprint/a"/> 19 <property name="user" value="sa"/> 20 <property name="password" value="sa"/> 21 </bean> 22 </constructor-arg> 23 </bean> 24 25 26 <bean id="dataSource2" class="org.h2.jdbcx.JdbcConnectionPool" 27 destroy-method="dispose"> 28 <constructor-arg> 29 <bean class="org.h2.jdbcx.JdbcDataSource"> 30 <property name="URL" value="jdbc:h2:r:/h2db/awbprint/b"/> 31 <property name="user" value="sa"/> 32 <property name="password" value="sa"/> 33 </bean> 34 </constructor-arg> 35 </bean> 36 37 <bean id="sqlSessionFactory1" class="org.mybatis.spring.SqlSessionFactoryBean"> 38 <property name="dataSource" ref="dataSource1"/> 39 <property name="configLocation" value="classpath:mybatis-config.xml"></property> 40 <property name="typeAliasesPackage" value="awbprint.mybatis.entity"></property> 41 <property name="mapperLocations" value="classpath:mybatis/a/**/*.xml"></property> 42 </bean> 43 44 <bean id="sqlSessionFactory2" class="org.mybatis.spring.SqlSessionFactoryBean"> 45 <property name="dataSource" ref="dataSource2"/> 46 <property name="configLocation" value="classpath:mybatis-config.xml"></property> 47 <property name="typeAliasesPackage" value="awbprint.mybatis.entity"></property> 48 <property name="mapperLocations" value="classpath:mybatis/b/**/*.xml"></property> 49 </bean> 50 51 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 52 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory1"></property> 53 <property name="basePackage" value="awbprint.mybatis.mapper.a"/> 54 </bean> 55 56 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 57 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory2"></property> 58 <property name="basePackage" value="awbprint.mybatis.mapper.b"/> 59 </bean> 60 61 62 </beans>
上面的配置,一個連h2的a資料庫,一個連h2的b資料庫,至於事務管理器,大家可參考這個思路,建二個,各管各的。
專案中mapper介面及對映檔案均用包名區分開,如下圖:
二、如何使用Map做為引數及動態條件生成
1 <resultMap id="BaseResultMap" type="awbprint.mybatis.entity.PrintLayout"> 2 <id column="D_RECID" property="recid" jdbcType="DECIMAL"/> 3 <result column="D_USER_NAME" property="userName" jdbcType="VARCHAR"/> 4 <result column="D_NAME" property="name" jdbcType="VARCHAR"/> 5 <result column="D_TYPE" property="type" jdbcType="VARCHAR"/> 6 ... 7 8 </resultMap> 9 10 <sql id="Base_Column_List"> 11 D_RECID, D_USER_NAME, D_NAME, D_TYPE, ... 12 </sql> 13 14 <select id="select" resultMap="BaseResultMap" 15 parameterType="java.util.Map"> 16 select 17 <include refid="Base_Column_List"/> 18 from T_PRINT_LAYOUT 19 where D_USER_NAME = #{userName,jdbcType=VARCHAR} and D_TYPE = #{awbType,jdbcType=VARCHAR} 20 <if test="recId != null"> 21 and D_RECID = #{recId,jdbcType=DECIMAL} 22 </if> 23 <if test="ids != null"> 24 or D_RECID in 25 <foreach item="item" index="index" collection="ids" 26 open="(" separator="," close=")"> 27 #{item} 28 </foreach> 29 </if> 30 31 </select>
14-31演示瞭如何使用Map做為引數,動態傳入查詢條件,及List引數生成in(...)條件
java端程式碼示例:
1 PrintLayoutMapper mapper = context.getBean(PrintLayoutMapper.class); 2 3 Map<String, Object> map = new HashMap<String, Object>(); 4 map.put("userName", "ADMIN"); 5 map.put("awbType", "CARGOLABEL_MU"); 6 map.put("recId", 1); 7 8 List<Integer> ids = new ArrayList<Integer>(); 9 ids.add(0, 1); 10 ids.add(0, 2); 11 ids.add(0, 3); 12 13 map.put("ids", ids); 14 15 List<?> list = mapper.select(map);
其實PrintLayoutMapper介面的定義為:
1 public interface PrintLayoutMapper { 2 ... 3 4 List<PrintLayout> select(Map<String, Object> map); 5 }
最終生成的SQL語句為:
1 select D_RECID, D_USER_NAME, D_NAME, D_TYPE, ... from T_PRINT_LAYOUT where D_USER_NAME = ? and D_TYPE = ? and D_RECID = ? or D_RECID in ( ? , ? , ? )
三、相容不同的資料庫
1 <insert id="insert"> 2 <selectKey keyProperty="id" resultType="int" order="BEFORE"> 3 <if test="_databaseId == 'oracle'"> 4 select seq_users.nextval from dual 5 </if> 6 <if test="_databaseId == 'db2'"> 7 select nextval for seq_users from sysibm.sysdummy1" 8 </if> 9 </selectKey> 10 insert into users values (#{id}, #{name}) 11 </insert>
這是官方文件上的示例,演示瞭如何相容oracle與db2這二種不同的資料庫,來獲取序列的下一個值
四、加強版的分支、選擇判斷
1 <select id="findActiveBlogLike" 2 resultType="Blog"> 3 SELECT * FROM BLOG WHERE state = ‘ACTIVE’ 4 <choose> 5 <when test="title != null"> 6 AND title like #{title} 7 </when> 8 <when test="author != null and author.name != null"> 9 AND author_name like #{author.name} 10 </when> 11 <otherwise> 12 AND featured = 1 13 </otherwise> 14 </choose> 15 </select>
這也是官方文件上的示例,因為<if>...</if>並沒對應的<else>標籤,所以要達到<if>...<else>...</else> </if>的效果,得藉助<choose>、<when>、<otherwise>組合使用。
五、避免Where 空條件的尷尬
1 <select id="findActiveBlogLike" resultType="Blog"> 2 SELECT * FROM BLOG 3 WHERE 4 <if test="state != null"> 5 state = #{state} 6 </if> 7 </select>
如果state引數為空時,最終生成SQL語句為
1 SELECT * FROM BLOG 2 WHERE
執行會出錯,當然,你可以在where 後加一個1=1,改成
1 <select id="findActiveBlogLike" resultType="Blog"> 2 SELECT * FROM BLOG 3 WHERE 1=1 4 <if test="state != null"> 5 and state = #{state} 6 </if> 7 </select>
但是這個做法不太“環保”(畢竟引入了一個垃圾條件),其實只要改成<where>...</where>即可
1 <select id="findActiveBlogLike" resultType="Blog"> 2 SELECT * FROM BLOG 3 <where> 4 <if test="state != null"> 5 and state = #{state} 6 </if> 7 </where> 8 </select>
六、$與#的區別
1 select * from T_PRINT_LAYOUT where D_RECID = ${recId}
最後生成的SQL為:
1 select * from T_PRINT_LAYOUT where D_RECID = 1
即:直接將引數值替換到了原來${recId}的位置,相當於硬拼SQL
1 select * from T_PRINT_LAYOUT where D_RECID = #{recid,jdbcType=DECIMAL}
最後生成的SQL為:
1 select * from T_PRINT_LAYOUT where D_RECID = ?
即:#{...}被識別為一個SQL引數
七、大量資料的批量insert
大量資料(條數>10000)做insert時,如果按常規方式,每條insert into table(...) values(...);來提交,速度巨慢。改善效能的思路是多條insert批量提交。
oracle環境中,有一種批量insert的小技巧,原理是 insert into ... select from ...,套在mybatis上,變形為:
1 INSERT INTO T_TEST 2 (ID, COL_A, COL_B) 3 SELECT SEQ_TEST.NEXTVAL, A.* 4 FROM ( 5 SELECT 'A1', 'B1' FROM DUAL 6 UNION ALL SELECT 'A2', 'B2' FROM DUAL 7 UNION ALL SELECT 'A3', 'B3' FROM DUAL 8 UNION ALL SELECT 'A4', 'B4' FROM DUAL 9 UNION ALL SELECT 'A5', 'B5' FROM DUAL 10 UNION ALL SELECT 'A6', 'B6' FROM DUAL 11 ) A
中間的部分非常有規律,可以用foreach標籤生成,參考下面的片段:
1 <insert id="insertBatch2" parameterType="ctas.entity.SharkFlt"> 2 <selectKey keyProperty="recId" order="BEFORE" resultType="Long"> 3 select SEQ_CTAS_SHARK_FLT.nextval as recId from dual 4 </selectKey> 5 insert into CTAS_SHARK_FLT (<include refid="Base_Column_List"/>) SELECT SEQ_TEST.NEXTVAL, A.* 6 FROM ( 7 <foreach collection="list" item="item" index="index" open="" close="" separator="union all"> 8 select #{item.awbType,jdbcType=VARCHAR}, #{item.awbPre,jdbcType=VARCHAR},... from dual 9 </foreach> 10 ) A 11 </insert>
即使這樣,也不能直接run,oracle中一次執行的sql語句長度是有限制的,如果最後拼出來的sql字串過長,會導致執行失敗,所以java端還要做一個分段處理,參考下面的處理:
1 List<SharkFlt> data = new ArrayList<SharkFlt>(); 2 for (TSharkFlt f : sharkFlts) { 3 data.add(getSharkFlt(f)); 4 } 5 6 System.out.println(data.size()); 7 8 long beginTime = System.currentTimeMillis(); 9 System.out.println("開始插入..."); 10 SqlSessionFactory sqlSessionFactory = ctx.getBean(SqlSessionFactory.class); 11 SqlSession session = null; 12 try { 13 session = sqlSessionFactory.openSession(ExecutorType.BATCH, false); 14 int a = 2000;//每次提交2000條 15 int loop = (int) Math.ceil(data.size() / (double) a); 16 17 List<SharkFlt> tempList = new ArrayList<SharkFlt>(a); 18 int start, stop; 19 for (int i = 0; i < loop; i++) { 20 tempList.clear(); 21 start = i * a; 22 stop = Math.min(i * a + a - 1, data.size() - 1); 23 System.out.println("range:" + start + " - " + stop); 24 for (int j = start; j <= stop; j++) { 25 tempList.add(data.get(j)); 26 } 27 session.insert("ctas.importer.writer.mybatis.mappper.SharkFltMapper.insertBatch2", tempList); 28 session.commit(); 29 session.clearCache(); 30 System.out.println("已經插入" + (stop + 1) + " 條"); 31 } 32 } catch (Exception e) { 33 e.printStackTrace(); 34 session.rollback(); 35 } finally { 36 if (session != null) { 37 session.close(); 38 } 39 } 40 long endTime = System.currentTimeMillis(); 41 System.out.println("插入完成,耗時 " + (endTime - beginTime) + " 毫秒!");
13,27-29這幾行是關鍵,這一段邏輯會經常使用,為了重用,可以封裝一下:
1 /** 2 * 批量提交資料 3 * @param sqlSessionFactory 4 * @param mybatisSQLId SQL語句在Mapper XML檔案中的ID 5 * @param commitCountEveryTime 每次提交的記錄數 6 * @param list 要提交的資料列表 7 * @param logger 日誌記錄器 8 */ 9 private <T> void batchCommit(SqlSessionFactory sqlSessionFactory, String mybatisSQLId, int commitCountEveryTime, List<T> list, Logger logger) { 10 SqlSession session = null; 11 try { 12 session = sqlSessionFactory.openSession(ExecutorType.BATCH, false); 13 int commitCount = (int) Math.ceil(list.size() / (double) commitCountEveryTime); 14 List<T> tempList = new ArrayList<T>(commitCountEveryTime); 15 int start, stop; 16 Long startTime = System.currentTimeMillis(); 17 for (int i = 0; i < commitCount; i++) { 18 tempList.clear(); 19 start = i * commitCountEveryTime; 20 stop = Math.min(i * commitCountEveryTime + commitCountEveryTime - 1, list.size() - 1); 21 for (int j = start; j <= stop; j++) { 22 tempList.add(list.get(j)); 23 } 24 session.insert(mybatisSQLId, tempList); 25 session.commit(); 26 session.clearCache(); 27 } 28 Long endTime = System.currentTimeMillis(); 29 logger.debug("batchCommit耗時:" + (endTime - startTime) + "毫秒"); 30 } catch (Exception e) { 31 logger.error("batchCommit error!", e); 32 e.printStackTrace(); 33 session.rollback(); 34 } finally { 35 if (session != null) { 36 session.close(); 37 } 38 } 39 }
對應的,如果是批量update,也是類似的思路,只不過要注意一點:oracle環境中,多條語句提交的sql語句為
begin
update xxx set xxx =xxx ;
update xxx set xxx =xxx;
end;
用mytais拼的時候,參考下面的寫法:
1 <update id="updateBatch" parameterType="java.util.List"> 2 <foreach collection="list" item="item" index="index" open="begin" close="end;" > 3 update xxx set x=#{item.x,jdbcType=VARCHAR} where x =#{item.x,jdbcType=VARCHAR}; 4 </foreach> 5 </update>
關於批量提交的效能,Oracle環境下,我大概測試了一下:
insert into ... select xxx
union all select yyy
union all select zzz;
最快,其次是
begin
insert into ... values ...;
insert into ... values ...;
end;
當然最慢是逐條insert提交,最後談下Spring與mybatis整合後,AOP事務管理 對 批量提交的影響 ,通常情況下,我們會這樣配置AOP事務管理:
1 <tx:advice id="txAdvice" transaction-manager="transactionManager"> 2 <tx:attributes> 3 <tx:method name="do*" read-only="false" rollback-for="java.lang.Exception"/> 4 <tx:method name="*" propagation="SUPPORTS" read-only="true"/> 5 </tx:attributes> 6 </tx:advice> 7 8 <aop:config> 9 <aop:pointcut id="pc" expression="execution(* ctas.service.*.*(..))"/> 10 <aop:advisor pointcut-ref="pc" advice-ref="txAdvice"/> 11 </aop:config>
這樣,ctas.service(及子包)下的所有方法都被攔截,而且只有do開頭的方法,具有可寫的事務(即:能insert/update/delete記錄),而其它方法是隻讀事務(即:只能select資料),但是我們前面談到的批量提交操作,都是寫程式碼手動提交的,不需要spring管理,所以配置中需要將某些方法排除,可以約定self開頭的方法,由開發者自己管理事務,不需要spring代為管理,上面的配置要改成:
1 <aop:config> 2 <aop:pointcut id="pc" expression="execution(* ctas.service.*.*(..)) and !execution(* ctas.service.*.self*(..)))"/> 3 <aop:advisor pointcut-ref="pc" advice-ref="txAdvice"/> 4 </aop:config>
通過 and !execution(...) 將self開頭的方法排除就可以了,前面的批量操作程式碼寫到selfXXX方法中。
關於批量提交,還有一種情況:父子表的批量插入。思路還是一樣的,但是SQL的寫法有點區別,原理參考下面的語句(Oracle環境)
1 DECLARE 2 BASE_ID INTEGER; 3 DETAIL_ID INTEGER; 4 BEGIN 5 --第1組記錄 6 SELECT SEQ_T_BASE.NEXTVAL INTO BASE_ID FROM DUAL; 7 INSERT INTO T_BASE (ID, FEE) VALUES (BASE_ID, ?); 8 9 SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL; 10 INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?); 11 SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL; 12 INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?); 13 14 --第2組記錄 15 SELECT SEQ_T_BASE.NEXTVAL INTO BASE_ID FROM DUAL; 16 INSERT INTO T_BASE (ID, FEE) VALUES (BASE_ID, ?); 17 18 SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL; 19 INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?); 20 SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL; 21 INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?); 22 23 --... 24 END;
xml對映檔案中的寫法:
1 <insert id="insertBatch" parameterType="java.util.List"> 2 DECLARE 3 base_id INTEGER ; 4 detail_id INTEGER ; 5 <foreach collection="list" item="item" index="index" open="begin" close="end;"> 6 select seq_t_base.nextval into base_id from dual; 7 insert into t_base(id, fee) values(base_id, #{item.baseEntity.fee,jdbcType=DECIMAL}); 8 <foreach collection="item.details" item="detail" index="index"> 9 select seq_t_detail.nextval into detail_id from dual; 10 insert into t_detail(id, base_id, fee) values(detail_id,base_id,#{detail.fee,jdbcType=DECIMAL}); 11 </foreach> 12 </foreach> 13 </insert>
List中的Dto定義
1 public class BaseDetailDto { 2 3 private TBase baseEntity; 4 5 private List<TDetail> details; 6 7 public TBase getBaseEntity() { 8 return baseEntity; 9 } 10 11 public void setBaseEntity(TBase baseEntity) { 12 this.baseEntity = baseEntity; 13 } 14 15 16 public List<TDetail> getDetails() { 17 return details; 18 } 19 20 public void setDetails(List<TDetail> details) { 21 this.details = details; 22 } 23 }