但凡使用mybatis,同時與spring集成使用時,接下來要說的這個問題是躲不了的。眾所周知,mybatis的SqlSessionFactory在獲取一個SqlSession時使用默認Executor或必須要指定一個Executor,這樣一來,在同一個SqlSession的生命周期中,要想切換Executor是不可能的,比如在一個復雜業務中:
sqlSession.insert("insertMainOrder", mainOrder); // -----(1) for(OrderInfo childOrder : childOrderList){ // -----循環插入是可以的 sqlSession.insert("insertChildOrder", childOrder); }
但是使用MyBatisBatchItemWriter是不行的,因為它使用了SqlSessionTemplate的batch屬性,官方解釋如下:
ItemWriter that uses the batching features from SqlSessionTemplate to execute a batch of statements for all itemsprovided.
以下是xml配置文件實現,也可以代碼實現:
<!--結果寫入庫--> <bean id="pickUpWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter" scope="step"> <property name="sqlSessionFactory" ref="sqlSessionFactory"/> <property name="statementId" value="com.cwenao.cc.basic.dao.NoticeInfoDao.insertSelective"/> </bean>
如果sqlSession使用ExecutorType.SIMPLE open出來的話,(2)處如果是用Jdbc batch操作將是不可能的,當然(2)處如果你再新open一個ExecutorType.BATCH的新的SqlSession的話:A、如果整個業務在無事務環境下運行的話,則不會報錯,但是底層會使用多個不同的Connection,浪費資源,最重要的是無法保持在同一個事務中。B、如果整個業務在一個事務中運行的話(如propagation=Propagation.REQUIRED),則會在mybatis-spring框架中報錯:TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction"),也就是標題中的錯誤,究其原因是因為在mybatis-spring框架中在有事務情況下SqlSession是通過sessionFactory與當前線程綁定的,新open出來的SqlSession會與上一個使用的SqlSession的ExecutorType進行比較,如果ExecutorType改變了,則直接報錯。
下面是stackoverflow的解釋:
Because it sais it: you can't change the executor type inside the transaction.
it looks like you've tried to batch-write something as the part of more broad transaction that includes other SQL operations, but that transaction was started with SIMPLE (default) or REUSE executor type.
It's obvious, that batch-write requires BATCH executor type, though once the transaction started, it's executor type can not be changed. So, perform your batch operations in separate transaction, or run nested transaction, if your RDBMS allows it.
首先了解下相關知識,mybatis的執行器有三種類型:
- ExecutorType.SIMPLE
這個類型不做特殊的事情,它只為每個語句創建一個PreparedStatement。
- ExecutorType.REUSE
這種類型將重復使用PreparedStatements。
- ExecutorType.BATCH
這個類型批量更新,性能更優,但batch模式也有自己的問題,比如在Insert操作時,在事務沒有提交之前,是沒有辦法獲取到自增的id,這在某型情形下是不符合業務要求的,而且假如有一條sql語句報錯,則整個事務回滾,雖然這條sql語句不是太重要。注意:在同一事務中batch模式和simple模式之間無法轉換。
使用方式:
java代碼中,創建模板的時候:
new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
或者
SqlSession session = getSqlSessionFactory().openSession(ExecutorType.BATCH);
xml文件配置:
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
<constructor-arg index="1" value="BATCH"/>
</bean>
下面是具體執行方法:
1,ExecutorType.SIMPLE:可以返回自增鍵,自增鍵會在事務提交后,自動設置到傳入的user對象中,只需要在mapper文件中,增加屬性: useGeneratedKeys="true" keyProperty="productId",在外部java代碼中添加循環語句,xml中就是單條數據插入:
<!-- 插入一個user --> <insert id="insertUser" parameterType="User" statementType="PREPARED" useGeneratedKeys="true" keyProperty="userId"> INSERT INTO user ( <include refid="userColumns" /> , create_time, update_time) VALUES (#{email}, #{pwd},#{nickname}, #{phone}, #{sign}, #{age}, #{birthday}, #{createTime}, now()) </insert>
2,ExecutorType.SIMPLE,借助foreach動態sql語句,使用Insert values(...),(...),(...) 的方式,這種方式無法取到自增鍵,外部java代碼中就不需要循環,在xml中使用循環,但是需要注意的是,該SQL語句不能在實現ItemWriter接口的類中調用,不然會報異常:Cannot change the ExecutorType when there is an existing transaction:
<!-- 批量插入user --> <insert id="insertUsers" parameterType="map" useGeneratedKeys="true" keyProperty="userId"> INSERT INTO user ( <include refid="userColumns" /> , create_time, update_time) VALUES <foreach collection="users" item="userCommand" index="index" separator=","> (#{userCommand.email}, #{userCommand.pwd},#{userCommand.nickname}, #{userCommand.phone}, #{userCommand.sign}, #{userCommand.age}, #{userCommand.birthday}, #{userCommand.sex}, #{userCommand.createTime}, now()) </foreach> </insert>
或者這樣寫在代碼中,不需要xml配置文件:
@Component("ledgerWriter") public class LedgerWriter implements ItemWriter<Ledger> { @Resource private NamedParameterJdbcTemplate jdbcTemplate; private static final String sql = "insert into tableA(a,b) values (:col1,:col2)"; /** * 寫入數據 * * @param ledgers */ public void write(List<? extends Ledger> ledgers) throws Exception { //將userDtoList轉化成BeanPropertySqlParameterSource[]數組 List<BeanPropertySqlParameterSource> userSourceList = new ArrayList<BeanPropertySqlParameterSource>(); for (UserDto userDto : userDtoList) { userSourceList.add(new BeanPropertySqlParameterSource(userDto)); } BeanPropertySqlParameterSource[] beanSources = userSourceList.toArray(new BeanPropertySqlParameterSource[userSourceList.size()]); jdbcTemplate.batchUpdate(sql, beanSources); } }
3,ExecutorType.BATCH,但是SqlSession的執行器類型一旦設置就無法動態修改,因為這個方法仍然需要包在事務中。所以如果在配置文件中設置了執行器為SIMPLE,當要使用BATCH執行器時,需要臨時獲取,只能在單獨的事務中進行:
SqlSession session = sqlSessionTemplate.getSqlSessionFactory() .openSession(ExecutorType.BATCH, false); try { UserDao batchUserDao = session.getMapper(UserDao.class); for (UserCommand user : users) { batchUserDao.insertUser(user); } session.commit(); // 清理緩存,防止溢出 session.clearCache(); // 添加位置信息 userLbsDao.insertUserLbses(users); } finally { session.close(); }
4,ExecutorType.BATCH,全部改成batch模式。但是沒有辦法獲取到自增的id,spring事務一起使用,將無法回滾,必須注意,最好單獨使用。需要用到一個類:MyBatisBatchItemWriter,它是批量執行更新操作。