基於MyBatis的批量插入更新實現
我的問題:如何在MyBatis下實現批量插入和更新操作?
我的困惑:
1、貌似MyBatis不支援在XML配置的SQL中帶有分號“;”,這就要求儘可能在配置中通過一條SQL語句實現;
2、不同資料庫可支援的批量插入SQL語句的寫法有差別,Oracle批量插入SQL語句如下:
寫法:"INSERT ALL INTO a表 VALUES(各個值) INTO a表 VALUES (其它值) INTO a表 VALUES(其它值) ....再跟一個SELECT 語句"
參考文章:http://blog.csdn.net/chenleixing/article/details/45165761/
3、在SQL語句中拼湊批量插入語句,資料量大了終究是不妥,且不方便獲取插入資料的ID值。
4、如果排除SQL語句實現批量插入的方案,則可以考慮採用java程式碼,jdbc或mybatis執行分批提交實現。
參考文章:http://www.cnblogs.com/robinjava77/p/5530681.html
我的實現:
權衡再三,決定採用MyBatis分批提交以實現批量操作,同時這也會使程式碼更具通用性。
批量執行器:
public abstract class BatchExecutor<T> { private List<T> list = null; public BatchExecutor(List<T> list) { this.list = list; } public void execute() throws Exception { if (CollectionUtils.isEmpty(list)) { return; } SqlSessionFactoryBean sqlSessionFactory = (SqlSessionFactoryBean) ContextUtils.getBean("sqlSessionFactory"); SqlSession batchSqlSession = null; try { batchSqlSession = sqlSessionFactory.getObject().openSession(ExecutorType.BATCH, false); BaseMapper baseMapper = batchSqlSession.getMapper(BaseMapper.class); int batchCount = 500; for (int index = 0; index < list.size(); index++) { T object = list.get(index); doExecute(object, baseMapper); if (index != 0 && index % batchCount == 0) { batchSqlSession.commit(); } } batchSqlSession.commit(); } catch (Exception e) { batchSqlSession.rollback(); throw e; } finally { if (batchSqlSession != null) { batchSqlSession.close(); } } } protected abstract void doExecute(T object, BaseMapper baseMapper) throws Exception; }
業務方法:
@Override public void batchAddNew(List<T> list) throws Exception { BatchExecutor<T> executor = new BatchExecutor<T>(list) { @Override protected void doExecute(T object, BaseMapper baseMapper) throws Exception { addNew(object, baseMapper); } }; executor.execute(); }
這篇文章總結寫得挺不錯的,摘錄參考如下:
----------------------------------------------------------------------------
前言:做一個數據同步專案,要求:同步資料不丟失的情況下,提高插入效能。
專案DB框架:Mybatis。DataBase:Oracle。
----------------------------------------------------------------------------
批量插入資料方式:
一、Mybatis 全域性設定批處理;
二、Mybatis 區域性設定批處理;
三、Mybatis foreach批量插入:
①SELECT UNION ALL;
②BEGIN INSERT INTO ...;INSERT INTO...;...;END;
四、java自帶的批處理插入;
五、其他方式
-----------------------------------------------------------------------------
先說結論:Mybatis(全域性/區域性)批處理和java自帶的批處理 效能上差不多,屬於最優處理辦法,我這邊各種測試後,最後採用Mybatis區域性批處理方式。
一、Mybatis 全域性設定批處理
先上Spring-Mybatis.xml 配置資訊
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" 4 xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans.xsd 7 http://www.springframework.org/schema/context 8 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> 9 10 <!-- 自動掃描(自動注入) --> 11 <context:annotation-config/> 12 <context:component-scan base-package="com.company.dao"/> 13 14 <!-- 動態資料來源 --> 15 <bean id="dataSource" class="com.company.dao.datasource.DataSource"> 16 <property name="myConfigFile" value="mySource.xml"/> 17 </bean> 18 19 <!-- mybatis配置 --> 20 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 21 <property name="dataSource" ref="dataSource"/> 22 <property name="mapperLocations" value="classpath*:mapper/*/*/*.xml"/> 23 <property name="configLocation" value="classpath:/mybatisConfig.xml"/> 24 </bean> 25 26 <!-- 自動建立對映器,不用單獨為每個 mapper對映--> 27 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 28 <property name="basePackage" value="com.company.dao.mapper"/> 29 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> 30 </bean> 31 32 <!-- 事務管理器配置,單資料來源事務 --> 33 <bean id="transactionManager" 34 class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 35 <property name="dataSource" ref="dataSource"/> 36 </bean> 37 38 <tx:annotation-driven transaction-manager="transactionManager"/> 39 40 </beans>
再上mybatisConfig.xml(在本專案中,我沒有設定setting。最終採用的區域性批處理,因此未設定全域性批處理,具體原因後面再說。)
1 <?mapper.xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> 3 <configuration> 4 5 <settings> 6 <!-- 配置預設的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新。--> 7 <setting name="defaultExecutorType" value="BATCH"/> 8 <!--詳見:http://www.mybatis.org/mybatis-3/zh/configuration.html--> 9 </settings> 10 11 <!-- 別名列表 --> 12 <typeAliases> 13 <!-- typeAliases 中的配置都是配置別名,在此就不貼出來了 --> 14 </typeAliases> 15 16 </configuration>
這樣子設定好後,在BaseService開放saveBatch(List<T> list)方法
1 @Override 2 public void save(List<T> list) { 3 for (int i = 0;i < list.size();i++){ 4 mapper.insert(list.get(i)); 5 } 6 } 7 8 @Override 9 public void saveBatch(List<T> list) { 10 int size = list.size(); 11 int unitNum = 500; 12 int startIndex = 0; 13 int endIndex = 0; 14 while (size > 0){ 15 if(size > unitNum){ 16 endIndex = startIndex+unitNum; 17 }else { 18 endIndex = startIndex+size; 19 } 20 List<T> insertData = list.subList(startIndex,endIndex); 21 save(insertData); 22 size = size - unitNum; 23 startIndex = endIndex; 24 } 25 }
雖然看上去是500條記錄,一次次INSERT INTO,但由於在全域性已經設定Mybatis是批處理執行器,所以這500條INSERT INTO只會與Oracle資料庫通訊一次。
全域性設定批處理的侷限性在哪裡呢?
先附上mybatis官方的討論列表中最很關鍵的一句:“If the BATCH executor is in use, the update counts are being lost. ”
設定全域性批處理後,DB裡的insert、Update和delete方法,都無法返回進行DML影響DB_TABLE的行數。
1.insert 無法返回影響的行數,這個好解決,一個批處理放在一個事務裡,記錄批處理失敗次數,總數-批處理失敗次數*單位批處理資料量,就能得到insert 影響DB_TABLE的行數;
2.但是update和delete就無法很簡單的去統計影響行數了,如果做反覆查詢,反而降低了效率,得不償失。
雖現在的專案尚未有需要反饋影響DB_TABLE行數的需求,但是為了更靈活,我們放棄了全域性批處理的方式。
!這裡提個疑問:為什麼Mybatis官方,不將批處理的選擇方式下沉到方法級別?方便開發者根據實際情況,靈活選擇。我覺得這是個可以改進的地方,如有機會,可看原始碼去進行改進。
---------------------------------------------------------------------------------------------------------
二、Mybatis區域性批處理方式
由於領導說全域性批處理方式,不夠靈活,不適宜專案所需,要另想辦法支援。但是java自帶的批處理,因為專案程式碼管理的要求,也不能採用。因此,在仔細閱讀官方文件後,設想自己能否獲取SQLSession後openSession,將這個會話設定為批處理呢?
1 SqlSession session = sqlSessionFactory.openSession(); 2 try { 3 BlogMapper mapper = session.getMapper(BlogMapper.class); 4 // do work 5 } finally { 6 session.close(); 7 }
現在你有一個 SqlSessionFactory,可以用來建立 SqlSession 例項。
SqlSessionFactory
SqlSessionFactory 有六個方法可以用來建立 SqlSession 例項。通常來說,如何決定是你 選擇下面這些方法時:
- Transaction (事務): 你想為 session 使用事務或者使用自動提交(通常意味著很多 資料庫和/或 JDBC 驅動沒有事務)?
- Connection (連線): 你想 MyBatis 獲得來自配置的資料來源的連線還是提供你自己
- Execution (執行): 你想 MyBatis 複用預處理語句和/或批量更新語句(包括插入和 刪除)
過載的 openSession()方法簽名設定允許你選擇這些可選中的任何一個組合。
1 SqlSession openSession() 2 SqlSession openSession(boolean autoCommit) 3 SqlSession openSession(Connection connection) 4 SqlSession openSession(TransactionIsolationLevel level) 5 SqlSession openSession(ExecutorType execType,TransactionIsolationLevel level) 6 SqlSession openSession(ExecutorType execType) 7 SqlSession openSession(ExecutorType execType, boolean autoCommit) 8 SqlSession openSession(ExecutorType execType, Connection connection) 9 Configuration getConfiguration();
因此出來了局部批處理第一套程式碼實現方式:
1 public static void sqlSession(List<Student> data) throws IOException { 2 String resource = "mybatis-dataSource.xml"; 3 InputStream inputStream = null; 4 SqlSession batchSqlSession = null; 5 try{ 6 inputStream = Resources.getResourceAsStream(resource); 7 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 8 batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); 9 int batchCount = 500;//每批commit的個數 10 for(int index = 0; index < data.size();index++){ 11 Student stu = data.get(index); 12 batchSqlSession.getMapper(Student.class).insert(stu); 13 if(index !=0 && index%batchCount == 0){ 14 batchSqlSession.commit(); 15 } 16 } 17 batchSqlSession.commit(); 18 }catch (Exception e){ 19 e.printStackTrace(); 20 }finally { 21 if(batchSqlSession != null){ 22 batchSqlSession.close(); 23 } 24 if(inputStream != null){ 25 inputStream.close(); 26 } 27 } 28 }
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 <configuration> 6 <environments default="development"> 7 <environment id="development"> 8 <transactionManager type="JDBC"/> 9 <dataSource type="POOLED"> 10 <property name="driver" value="${driver}"/> 11 <property name="url" value="${url}"/> 12 <property name="username" value="${username}"/> 13 <property name="password" value="${password}"/> 14 </dataSource> 15 </environment> 16 </environments> 17 <mappers> 18 <mapper resource="org/mybatis/example/Student.xml"/> 19 </mappers> 20 </configuration>
已經在Spring-Mybatis.xml 中配置了SQLSessionFactory,那我為何還要自己去建立SQLSessionFactory呢?因此繼續改良程式碼
1 public static void mybatisSqlSession(List<Student> data){ 2 DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory) ServiceBeanConstant.CTX.getBean("sqlSessionFactory"); 3 SqlSession batchSqlSession = null; 4 try{ 5 batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); 6 int batchCount = 500;//每批commit的個數 7 for(int index = 0; index < data.size();index++){ 8 Student stu = data.get(index); 9 batchSqlSession.getMapper(StudentMapper.class).insert(stu); 10 if(index !=0 && index%batchCount == 0){ 11 batchSqlSession.commit(); 12 } 13 } 14 batchSqlSession.commit(); 15 }catch (Exception e){ 16 e.printStackTrace(); 17 }finally { 18 if(batchSqlSession != null){ 19 batchSqlSession.close(); 20 } 21 } 22 }
這個版本的區域性批處理插入是比較滿意的,最終採用的方式也是