數據庫使用的是MySQL,JDK版本1.8,運行在SpringBoot環境下
本文章源代碼:https://github.com/runbeyondmove/mybatis-batch-demo
對比3種可用的方式
1、反復執行單條插入語句
2、xml拼接sql
3、批處理執行
先說結論:少量插入請使用反復插入單條數據,方便。數量較多請使用批處理方式。(可以考慮以有需求的插入數據量20條左右為界吧,在我的測試和數據庫環境下耗時都是百毫秒級的,方便最重要)。無論何時都不用xml拼接sql的方式。
1. xml映射文件中的代碼
<insert id="insert" parameterType="top.spanrun.bootssm.model.UserInf" useGeneratedKeys="true" keyProperty="id"><!--@mbggenerated generator自動生成,注意order的before和after--><!--<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">SELECT LAST_INSERT_ID()</selectKey>-->insert into user_inf (id, uname, passwd, gentle, email, city)values (#{id,jdbcType=INTEGER}, #{uname,jdbcType=VARCHAR}, #{passwd,jdbcType=VARCHAR}, #{gentle,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{city,jdbcType=VARCHAR})</insert><insert id="insertWithXML" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">insert into user_inf (id, uname, passwd, gentle, email, city)values<foreach collection="list" item="user" index="index" separator=",">(#{user.id,jdbcType=INTEGER}, #{user.uname,jdbcType=VARCHAR}, #{user.passwd,jdbcType=VARCHAR},#{user.gentle,jdbcType=VARCHAR}, #{user.email,jdbcType=VARCHAR}, #{user.city,jdbcType=VARCHAR})</foreach></insert>
?
2.?Mapper接口
@Mapper
public interface UserInfMapper {int insert(UserInf record);int insertWithXML(@Param("list") List<UserInf> list);
}
?
3. Service實現,接口聲明省略
@Service
public class UserInfServiceImpl implements UserInfService{private static final Logger LOGGER = LoggerFactory.getLogger(UserInfServiceImpl.class);@AutowiredSqlSessionFactory sqlSessionFactory;@AutowiredUserInfMapper userInfMapper;@Transactional@Overridepublic boolean testInsertWithBatch(List<UserInf> list) {LOGGER.info(">>>>>>>>>>>testInsertWithBatch start<<<<<<<<<<<<<<");SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);UserInfMapper mapper = sqlSession.getMapper(UserInfMapper.class);long startTime = System.nanoTime();try {List<UserInf> userInfs = Lists.newArrayList();for (int i = 0; i < list.size(); i++) {// 每1000條提交一次
if ((i+1)%1000 == 0){sqlSession.commit();sqlSession.clearCache();}mapper.insert(list.get(i));}} catch (Exception e) {e.printStackTrace();} finally {sqlSession.close();}LOGGER.info("testInsertWithBatch spend time:{}",System.nanoTime()-startTime);LOGGER.info(">>>>>>>>>>>testInsertWithBatch end<<<<<<<<<<<<<<");return true;}@Transactional@Overridepublic boolean testInsertWithXml(List<UserInf> list) {LOGGER.info(">>>>>>>>>>>testInsertWithXml start<<<<<<<<<<<<<<");long startTime = System.nanoTime();userInfMapper.insertWithXML(list);LOGGER.info("testInsertWithXml spend time:{}",System.nanoTime()-startTime);LOGGER.info(">>>>>>>>>>>testInsertWithXml end<<<<<<<<<<<<<<");return true;}@Transactional@Overridepublic boolean testInsertWithForeach(List<UserInf> list) {LOGGER.info(">>>>>>>>>>>testInsertWithForeach start<<<<<<<<<<<<<<");long startTime = System.nanoTime();for (int i = 0; i < list.size(); i++) {userInfMapper.insert(list.get(i));}LOGGER.info("testInsertWithForeach spend time:{}",System.nanoTime()-startTime);LOGGER.info(">>>>>>>>>>>testInsertWithForeach end<<<<<<<<<<<<<<");return true;}@Transactional@Overridepublic boolean testInsert(UserInf userInf) {LOGGER.info(">>>>>>>>>>>testInsert start<<<<<<<<<<<<<<");long startTime = System.nanoTime();LOGGER.info("insert before,id=" + userInf.getId());userInfMapper.insert(userInf);LOGGER.info("insert after,id=" + userInf.getId());LOGGER.info("testInsert spend time:{}",System.nanoTime()-startTime);LOGGER.info(">>>>>>>>>>>testInsert end<<<<<<<<<<<<<<");return true;}
}
4.?Controller控制器
@RestController
public class UserInfController {@AutowiredUserInfService userInfService;@RequestMapping(value = "test/{size}/{type}")public void testInsert(@PathVariable(value = "size") Integer size,@PathVariable(value = "type") Integer type){System.out.println(">>>>>>>>>>>>type = " + type + "<<<<<<<<<<<<<");switch (type){case 1:userInfService.testInsertWithForeach(generateList(size));break;case 2:userInfService.testInsertWithXml(generateList(size));break;case 3:userInfService.testInsertWithBatch(generateList(size));break;default:UserInf userInf = new UserInf();userInf.setUname("user_single");userInf.setGentle("1");userInf.setEmail("123@123.com");userInf.setCity("廣州市");userInf.setPasswd("123456");userInfService.testInsert(userInf);}}private List<UserInf> generateList(int listSize){List<UserInf> list = Lists.newArrayList();UserInf userInf = null;for (int i = 0; i < listSize; i++) {userInf = new UserInf();userInf.setUname("user_" + i);userInf.setGentle("1");userInf.setEmail("123@123.com");userInf.setCity("廣州市");userInf.setPasswd("123456");list.add(userInf);}return list;}
}
測試結果(單位是納秒):
1000
testInsertWithForeach spend time:431526521
testInsertWithXml spend time:118772867
testInsertWithBatch spend time:17560234610000
testInsertWithForeach spend time:2072525050
testInsertWithXml spend time:685605121
testInsertWithBatch spend time:894647254100000
testInsertWithForeach spend time:18950160161
testInsertWithBatch spend time:8469312537testInsertWithXml報錯
### Cause: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (9388970 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.
; Packet for query is too large (9388970 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.; nested exception is com.mysql.jdbc.PacketTooBigException: Packet for query is too large
(9388970 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.] with root causecom.mysql.jdbc.PacketTooBigException: Packet for query is too large (9388970 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.
查看xml?sql拼接的異常信息,可以發現,最大只能達到4194304,也就是4M,所以這種方式不推薦
結論
循環插入單條數據雖然效率極低,但是代碼量極少,如果在使用tk.Mapper的插件情況下,僅需代碼,:
@Transactional
public void add1(List<Item> itemList) {itemList.forEach(itemMapper::insertSelective);
}
因此,在需求插入數據數量不多的情況下肯定用它了。
xml拼接sql是最不推薦的方式,使用時有大段的xml和sql語句要寫,很容易出錯,工作效率很低。更關鍵點是,雖然效率尚可,但是真正需要效率的時候你掛了,要你何用?
批處理執行是有大數據量插入時推薦的做法,使用起來也比較方便。
?
其他在使用中的補充:
1.?使用mybatis?generator生成器生成中的一些坑
代碼說明:數據庫是MySQL,且主鍵自增,用generator 生成的mapper.xml中的代碼,自增ID,使用的是selectKey來獲取。
問題描述:insert的時候,添加的時候,第一條數據添加成功,接著添加第二條數據的時候會提示失敗,失敗的原因是ID還是使用的上一個ID值,主鍵重復導致插入失敗。異常如下:
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '4' for key 'PRIMARY'
問題原因:BEFORE還是AFTER的問題
<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Integer">SELECT LAST_INSERT_ID()
</selectKey>
需要注意的是,Oracle使用before,MySQL使用after
其實在使用Mybatis?generator生成帶代碼的時候可以通過identity="true"來指定生成的selectKey是before還是after
<generatedKey column="id" sqlStatement="Mysql" identity="true" />
注:在select標簽中使用useGeneratedKeys="true"?keyProperty="id" 不存在該問題。
?
2. mybatis的版本
升級Mybatis版本到3.3.1
?
3.?在批量插入的拼接xml?sql時注意foreach是沒有使用open和close的,但是在批量查詢修改刪除時才使用到open和close
<foreach collection="list" item="user" index="index" separator=",">(#{user.id,jdbcType=INTEGER}, #{user.uname,jdbcType=VARCHAR}, #{user.passwd,jdbcType=VARCHAR},#{user.gentle,jdbcType=VARCHAR}, #{user.email,jdbcType=VARCHAR}, #{user.city,jdbcType=VARCHAR})</foreach>
?4. 使用批量提交注意的事項
a. 事務
由于在 Spring 集成的情況下,事務連接由 Spring 管理(SpringManagedTransaction
),所以這里不需要手動關閉?sqlSession
,在這里手動提交(commit
)或者回滾(rollback
)也是無效的。
? b. 批量提交
批量提交只能應用于 insert, update, delete。
并且在批量提交使用時,如果在操作同一SQL時中間插入了其他數據庫操作,就會讓批量提交方式變成普通的執行方式,所以在使用批量提交時,要控制好 SQL 執行順序
?