MyBatis中批量插入
方法一:
<insert id="insertbatch" parameterType="java.util.List"> <selectKey keyProperty="fetchTime" order="BEFORE" resultType="java.lang.String"> SELECT CURRENT_TIMESTAMP() </selectKey> insert into kangaiduoyaodian ( depart1, depart2, product_name, generic_name, img, product_specification, unit, approval_certificate, manufacturer, marketPrice, vipPrice, website, fetch_time, productdesc ) values <foreach collection="list" item="item" index="index" separator=","> ( #{item.depart1}, #{item.depart2}, #{item.productName}, #{item.genericName}, #{item.img}, #{item.productSpecification}, #{item.unit}, #{item.approvalCertificate}, #{item.manufacturer}, #{item.marketprice}, #{item.vipprice}, #{item.website}, #{fetchTime}, #{item.productdesc} ) </foreach> </insert>
方法二:
<insert id="batchInsertB2B" parameterType="ArrayList"> insert into xxxxtable(hkgs,hkgsjsda,office,asdf,ddd,ffff,supfullName,classtype,agent_type,remark) <foreach collection="list" item="item" index="index" separator="union all"> select #{item.hkgs,jdbcType=VARCHAR}, #{item.hkgsjsda,jdbcType=VARCHAR}, #{item.office,jdbcType=VARCHAR}, #{item.asdf,jdbcType=VARCHAR}, #{item.ddd,jdbcType=VARCHAR}, #{item.ffff,jdbcType=VARCHAR}, #{item.supfullName,jdbcType=VARCHAR},0,0, #{item.remark,jdbcType=VARCHAR} from dual </foreach> </insert>
可以考慮用union all來實現批量插入。
例如:
insert into XX_TABLE(XX,XX,XX)select 'xx','xx','xx' union all select 'xx','xx','xx' union all select 'xx','xx','xx' ...
先拼裝好語句再動態傳入insert into XX_TABLE(XX,XX,XX)后面部分
MyBatis中批量刪除
<!-- 通過主鍵集合批量刪除記錄 --> <delete id="batchRemoveUserByPks" parameterType="java.util.List"> DELETE FROM LD_USER WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </delete>
MyBatis中in子句
1.只有一個參數
參數的類型要聲明為List或Array
Sql配置如下:
<select id="selectProduct" resultMap="Map"> SELECT * FROM PRODUCT WHERE PRODUCTNO IN <foreach item="productNo" index="index" collection="參數的類型List或array"> #{productNo} </foreach> </select>
2.多個參數
首先要將多個參數寫入同一個map,將map作為一個參數傳入mapper
Sql配置如下:
<select id="selectProduct" resultMap="Map"> SELECT * FROM PRODUCT WHERE PRODUCTNO IN <foreach item="productNo" index="index" collection="map中集合參數的名稱"> #{productNo} </foreach> </select>
MyBatis批量修改
<update id="updateOrders" parameterType="java.util.List"> update orders set state = '0' where no in <foreach collection="list" item="nos" open="(" separator="," close=")"> #{nos} </foreach> </update>
- MyBatis的前身就是著名的Ibatis,不知何故脫離了Apache改名為MyBatis。
MyBatis所說是輕量級的ORM框架,在網上看過一個測試報告,感覺相比於Hibernate來說,優勢並不明顯。
下面說一下比較有趣的現象,根據MyBatis的官方文檔,在獲得sqlSession時,它有為批量更新而專門准備的:
session = sessionFactory.openSession(ExecutorType.BATCH, true);//用於批量update
一 般來說,對MYSQL數據庫批量操作時速度取決於,是為每一個處理分別建立一個連接,還是為這一批處理一共建立一個連接。按MyBatis的手冊說明,選 擇ExecutorType.BATCH意味着,獲得的sqlSession會批量執行所有更新語句。不過我測試了一下,批量插入1000條數據,發覺 ExecutorType.BATCH方式的效率居然比普通的方式差很多。我測試用的Mapper中的insert配置如下,再用for循環插入1000條記錄:
2 <!-- WARNING - @mbggenerated This element is automatically generated by
3 MyBatis Generator, do not modify. This element was generated on Mon May 09
4 11:09:37 CST 2011. -->
5 insert into student (id, name, sex,
6 address, telephone, t_id
7 )
8 values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR},
9 #{sex,jdbcType=VARCHAR},
10 #{address,jdbcType=VARCHAR}, #{telephone,jdbcType=VARCHAR}, #{tId,jdbcType=INTEGER}
11 )
12 </insert>
- 我不清楚原因在哪里, 就配置了MyBatis的log4j,想查看下日志。下載了log4j.jar和commons-logging.jar並配置到項目的類路徑,然后在代碼路徑下新建文件log4j.properties,內容如下:
log4j.rootLogger=DEBUG, stdout
# SqlMap logging configuration...
log4j.logger.com.ibatis=DEBUG
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
log4j.logger.com.ibatis.sqlmap.engine.cache.CacheModel=DEBUG
log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientImpl=DEBUG
log4j.logger.com.ibatis.sqlmap.engine.builder.xml.SqlMapParser=DEBUG
log4j.logger.com.ibatis.common.util.StopWatch=DEBUG
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
DEBUG [main] - Created connection 3502256.
DEBUG [main] - ooo Connection Opened
DEBUG [main] - ==> Executing: insert into student ( name, sex, address, telephone, t_id ) values ( ?, ?, ?, ?, ? )
DEBUG [main] - ==> Parameters: 新人0(String), male(String), addr0(String), dd(String),3(Integer)
DEBUG [main] - ==> Executing: insert into student ( name, sex, address, telephone, t_id ) values ( ?, ?, ?, ?, ? )
DEBUG [main] - ==> Parameters: 新人1(String), male(String),
...............
...............
DEBUG [main] - xxx Connection Closed
DEBUG [main] - Returned connection 3502256 to pool. - 最后一點是關於數據庫批量插入時sql語句級的優化,我特意測試了兩種方式,在StudentMapper中配置了兩種 insert模式。第一種對應insert value1,insert value2,,,,;第二種對應insert values (value1, value2,....)。發現后者果然比前者快很多啊。下面是兩種insert模式,及測試結果對應圖:
<!-- 在外部for循環調用一千次 -->
<insert id="insert" parameterType="sdc.mybatis.test.Student">
insert into student (id, name, sex,
address, telephone, t_id
)
values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR},
#{sex,jdbcType=VARCHAR},
#{address,jdbcType=VARCHAR}, #{telephone,jdbcType=VARCHAR}, #{tId,jdbcType=INTEGER}
)
</insert>
<!-- 批量 ,傳入一個長度為1000的list -->
<insert id="insertBatch">
insert into student ( <include refid="Base_Column_List"/> )
values
<foreach collection="list" item="item" index="index" separator=",">
(null,#{item.name},#{item.sex},#{item.address},#{item.telephone},#{item.tId})
</foreach>
</insert>
過Mybatis做7000+數據量的批量插入的時候報錯了,error log如下:
可以看到這種異常無法捕捉,僅能看到異常指向了druid和ibatis的原碼處,初步猜測是由於默認的SqlSession無法支持這個數量級的批量操作,下面就結合源碼和官方文檔具體看一看。
源碼分析
項目使用的是spring+Mybatis,在Dao層是通過Spring提供的SqlSessionTemplate來獲取SqlSession的:
@Resource(name = "sqlSessionTemplate") private SqlSessionTemplate sqlSessionTemplate; public SqlSessionTemplate getSqlSessionTemplate() { return sqlSessionTemplate; }
為了驗證,接下看一下它是如何提供SqlSesion的,打開SqlSessionTemplate的源碼,看一下它的構造方法:
SqlSessionTemplate.java
/** * Constructs a Spring managed SqlSession with the {@code SqlSessionFactory} * provided as an argument. * * @param sqlSessionFactory */ public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); }
接下來再點開getDefaultExecutorType這個方法:
Configuration.java
public ExecutorType getDefaultExecutorType() { return defaultExecutorType; }
可以看到它直接返回了類中的全局變量defaultExecutorType
,我們再在類的頭部尋找一下這個變量:
Configuration.java
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
找到了,Spring為我們提供的默認執行器類型為Simple,它的類型一共有三種:
package org.apache.ibatis.session; /** * @author Clinton Begin */ public enum ExecutorType { SIMPLE, REUSE, BATCH }
仔細觀察一下,發現有3個枚舉類型,其中有一個BATCH是否和批量操作有關呢?我們看一下mybatis官方文檔中對這三個值的描述:
- ExecutorType.SIMPLE: 這個執行器類型不做特殊的事情。它為每個語句的執行創建一個新的預處理語句。
- ExecutorType.REUSE: 這個執行器類型會復用預處理語句。
- ExecutorType.BATCH:這個執行器會批量執行所有更新語句,如果 SELECT 在它們中間執行還會標定它們是 必須的,來保證一個簡單並易於理解的行為。
可以看到我的使用的SIMPLE會為每個語句創建一個新的預處理語句,也就是創建一個PreparedStatement對象,即便我們使用druid連接池進行處理,依然是每次都會向池中put一次並加入druid的cache中。這個效率可想而知,所以那個異常也有可能是insert timeout導致等待時間超過數據庫驅動的最大等待值。
好了,已解決問題為主,根據分析我們選擇通過BATCH
的方式來創建SqlSession,官方也提供了一系列重載方法:
public interface SqlSessionFactory { SqlSession openSession(); SqlSession openSession(boolean autoCommit); SqlSession openSession(Connection connection); SqlSession openSession(TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType); SqlSession openSession(ExecutorType execType, boolean autoCommit); SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType, Connection connection); Configuration getConfiguration(); }
可以觀察到主要有四種參數類型,分別是
- Connection connection
- ExecutorType execType
- TransactionIsolationLevel level
- boolean autoCommit
官方文檔中對這些參數也有詳細的解釋:
SqlSessionFactory 有六個方法可以用來創建 SqlSession 實例。通常來說,如何決定是你 選擇下面這些方法時:
Transaction (事務): 你想為 session 使用事務或者使用自動提交(通常意味着很多 數據庫和/或 JDBC 驅動沒有事務)?
Connection (連接): 你想 MyBatis 獲得來自配置的數據源的連接還是提供你自己
Execution (執行): 你想 MyBatis 復用預處理語句和/或批量更新語句(包括插入和 刪除)?
所以根據需求選擇即可,由於我們要做的事情是批量insert,所以我們選擇SqlSession openSession(ExecutorType execType, boolean autoCommit)
順帶一提關於TransactionIsolationLevel也就是我們經常提起的事務隔離級別,官方文檔中也介紹的很到位:
MyBatis 為事務隔離級別調用使用一個 Java 枚舉包裝器, 稱為 TransactionIsolationLevel,
package org.apache.ibatis.session; import java.sql.Connection; /** * @author Clinton Begin */ public enum TransactionIsolationLevel { NONE(Connection.TRANSACTION_NONE), READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED), READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED), REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);否則它們按預期的方式來工作,並有 JDBC 支持的 5 級 (
NONE,
READ_UNCOMMITTED
READ_COMMITTED,
REPEATABLE_READ,
SERIALIZA BLE)
解決問題
回歸正題,初步找到了問題原因,那我們換一中SqlSession的獲取方式再試試看。
不幸的是,依舊報相同的錯誤,看來不僅僅是ExecutorType的問題,那會不會是一次commit的數據量過大導致響應時間過長呢?上面我也提到了這種可能性,那么就再分批次處理試試,也就是說,在同一事務范圍內,分批commit insert batch。具體看一下Dao層的代碼實現:
@Override public boolean insertCrossEvaluation(List<CrossEvaluation> members) throws Exception { // TODO Auto-generated method stub int result = 1; SqlSession batchSqlSession = null; try { batchSqlSession = this.getSqlSessionTemplate() .getSqlSessionFactory() .openSession(ExecutorType.BATCH, false);// 獲取批量方式的sqlsession int batchCount = 1000;// 每批commit的個數 int batchLastIndex = batchCount;// 每批最后一個的下標 for (int index = 0; index < members.size();) { if (batchLastIndex >= members.size()) { batchLastIndex = members.size(); result = result * batchSqlSession.insert("MutualEvaluationMapper.insertCrossEvaluation",members.subList(index, batchLastIndex)); batchSqlSession.commit(); System.out.println("index:" + index+ " batchLastIndex:" + batchLastIndex); break;// 數據插入完畢,退出循環 } else { result = result * batchSqlSession.insert("MutualEvaluationMapper.insertCrossEvaluation",members.subList(index, batchLastIndex)); batchSqlSession.commit(); System.out.println("index:" + index+ " batchLastIndex:" + batchLastIndex); index = batchLastIndex;// 設置下一批下標 batchLastIndex = index + (batchCount - 1); } } batchSqlSession.commit(); } finally { batchSqlSession.close(); } return Tools.getBoolean(result); }
再次測試,程序沒有報異常,總共7728條數據 insert的時間大約為10s左右,如下圖所示,
轉自:http://blog.csdn.net/wlwlwlwl015/article/details/50246717
總結:
mysql數據庫在操作時,sql的長度是有限制的。所以在批處理時分批要合理避免出現sql過長的限制。可以用多線程執行批處理,提高速度。