Mybatis之批處理DML操作

1 批處理插入更新

1.1 簡(jiǎn)介

1.1.1 定義

處理批處理的方式有很多種窗骑,這里不分析各種方式的優(yōu)劣擎淤,只是概述 ExecutorType.BATCH這種的用法

簡(jiǎn)單了解一下批處理背后的秘密,BatchExecutor批處理是 JDBC 編程中的另一種優(yōu)化手段。JDBC 在執(zhí)行 SQL 語句時(shí),會(huì)將 SQL 語句以及實(shí)參通過網(wǎng)絡(luò)請(qǐng)求的方式發(fā)送到數(shù)據(jù)庫官卡,一次執(zhí)行一條 SQL 語句,一方面會(huì)減小請(qǐng)求包的有效負(fù)載醋虏,另一個(gè)方面會(huì)增加耗費(fèi)在網(wǎng)絡(luò)通信上的時(shí)間寻咒。

通過批處理的方式,我們就可以在 JDBC 客戶端緩存多條 SQL語句颈嚼,然后在 flush 或緩存滿的時(shí)候毛秘,將多條 SQL 語句打包發(fā)送到數(shù)據(jù)庫執(zhí)行,這樣就可以有效地降低上述兩方面的損耗粘舟,從而提高系統(tǒng)性能熔脂。

不過貌虾,有一點(diǎn)需要特別注意:

每次向數(shù)據(jù)庫發(fā)送的 SQL 語句的條數(shù)是有上限的郭计,如果批量執(zhí)行的時(shí)候超過這個(gè)上限值,數(shù)據(jù)庫就會(huì)拋出異常癣猾,拒絕執(zhí)行這一批 SQL 語句晰骑,所以我們需要控制批量發(fā)送 SQL 語句的條數(shù)頻率

1.1.2 ExecutorType.BATCH使用步驟

以下是如何在 MyBatis 中使用 ExecutorType.BATCH 的示例:

  • 需要獲取一個(gè)批處理類型的 SqlSession
SqlSessionFactory sqlSessionFactory = ... // 獲取 SqlSessionFactory
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
  • 通過這個(gè) SqlSession 獲取 Mapper适秩,并執(zhí)行批處理操作:
YourMapper mapper = sqlSession.getMapper(YourMapper.class);
for (YourEntity entity : entities) {
    mapper.insert(entity); // 或者 update/delete
}
  • 手動(dòng)提交事務(wù),并關(guān)閉 SqlSession
sqlSession.commit();
sqlSession.close();

1.1.3 注意事項(xiàng)

需要注意的是:

  • 在使用 ExecutorType.BATCH 時(shí)硕舆,MyBatis 不會(huì)立即執(zhí)行 SQL秽荞,而是將 SQL 緩存起來,等到調(diào)用 SqlSession.commit()SqlSession.flushStatements() 時(shí)才會(huì)一次性執(zhí)行抚官。
  • 如果在批處理過程中出現(xiàn)異常扬跋,需要確保進(jìn)行適當(dāng)?shù)腻e(cuò)誤處理,例如回滾事務(wù)凌节。
  • 在使用 ExecutorType.BATCH 時(shí)钦听,insert、update 和 delete 操作返回的結(jié)果可能不正確(通常為 -2147482646)倍奢。如果需要獲取這些操作的結(jié)果朴上,可以在 SqlSession.flushStatements() 之后調(diào)用 SqlSession.clearCache(),然后再執(zhí)行查詢操作卒煞。
  • 如果批處理的數(shù)據(jù)量非常大痪宰,可能需要在每處理一定數(shù)量的數(shù)據(jù)后調(diào)用一次 SqlSession.flushStatements()SqlSession.clearCache(),以避免內(nèi)存溢出畔裕。

1.2 JDBC使用批量

使用Batch批量處理數(shù)據(jù)庫衣撬,當(dāng)需要向數(shù)據(jù)庫發(fā)送一批SQL語句執(zhí)行時(shí),應(yīng)避免向數(shù)據(jù)庫一條條的發(fā)送執(zhí)行柴钻,而應(yīng)采用JDBC的批處理機(jī)制淮韭,以提升執(zhí)行效率

1.2.1 Statement批處理

使用Statement批處理

Statement.addBatch(sql) list 執(zhí)行批處理SQL語句
executeBatch()方法:執(zhí)行批處理命令
clearBatch()方法:清除批處理命令

Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JDBCManager.getConnection();
String sql1 = "insert into user(name,password,email,birthday) 
values('kkk','123','abc@sina.com','1978-08-08')";
String sql2 = "update user set password='自定義密碼' where id=3";
st = conn.createStatement();
st.addBatch(sql1);  //把SQL語句加入到批命令中
st.addBatch(sql2);  //把SQL語句加入到批命令中
st.executeBatch();
} finally{
JDBCManager.DBclose(con,st,rs);
}

采用Statement.addBatch(sql)方式實(shí)現(xiàn)批處理:
優(yōu)點(diǎn):可以向數(shù)據(jù)庫發(fā)送多條不同的SQL語句。
缺點(diǎn):SQL語句沒有預(yù)編譯贴届。
當(dāng)向數(shù)據(jù)庫發(fā)送多條語句相同靠粪,但僅參數(shù)不同的SQL語句時(shí),需重復(fù)寫上很多條SQL語句毫蚓。例如:

Insert into user(name,password) values(‘a(chǎn)a’,’111’);

Insert into user(name,password) values(‘bb’,’222’);
Insert into user(name,password) values(‘cc’,’333’);
Insert into user(name,password) values(‘dd’,’444’);

1.2.2 PreparedStatement批處理

PreparedStatement批處理

PreparedStatement.addBatch();

conn = JDBCManager.getConnection();//獲取工具占键;
String sql = "insert into user(name,password,email,birthday) values(?,?,?,?)";
st = conn.prepareStatement(sql);//預(yù)處理sql語句;
for(int i=0;i<50000;i++){
st.setString(1, "aaa" + i);
st.setString(2, "123" + i);
st.setString(3, "aaa" + i + "@sina.com");
st.setDate(4,new Date(1980, 10, 10));
 
st.addBatch();//將一組參數(shù)添加到此 PreparedStatement 對(duì)象的批處理命令中元潘。
if(i%1000==0){
st.executeBatch();
st.clearBatch();清空此 Statement 對(duì)象的當(dāng)前 SQL 命令列表畔乙。 
}
}

st.executeBatch();將一批命令提交給數(shù)據(jù)庫來執(zhí)行,如果全部命令執(zhí)行成功翩概,則返回更新計(jì)數(shù)組成的數(shù)組牲距。返回?cái)?shù)組的 int 元素的排序?qū)?yīng)于批中的命令返咱,批中的命令根據(jù)被添加到批中的順序排序

采用PreparedStatement.addBatch()實(shí)現(xiàn)批處理
優(yōu)點(diǎn):發(fā)送的是預(yù)編譯后的SQL語句,執(zhí)行效率高牍鞠。
缺點(diǎn):只能應(yīng)用在SQL語句相同咖摹,但參數(shù)不同的批處理中。因此此種形式的批處理經(jīng)常用于在同一個(gè)表中批量插入數(shù)據(jù)难述,或批量更新表的數(shù)據(jù)萤晴。

1.3 Mybatis初級(jí)使用批量

廢話不多說,早先時(shí)候項(xiàng)目的代碼里就已經(jīng)存在了批處理的代碼胁后,偽代碼的樣子大概是這樣子的:

@Autowired
private SqlSessionFactory sqlSessionFactory;
@Resource
private TestMapper testMapper;

private int BATCH = 1000;
  private void doUpdateBatch(Date accountDate, List<某實(shí)體類> data) {
   
    try (SqlSession batchSqlSession =sqlSessionFactory.openSession(ExecutorType.BATCH, false);){
      if (data == null || data.size() == 0) {
        return;
      }
    TestMapper  mapper = batchSqlSession .getMapper(TestMapper.class);
      for (int index = 0; index < data.size(); index++) {
        mapper.更新/插入Method(accountDate, data.get(index).getOrderNo());
        if (index != 0 && index % BATCH == 0) {
          batchSqlSession.commit();
          batchSqlSession.clearCache();
        }
      }
      batchSqlSession.commit();
    } catch (Exception e) {
      batchSqlSession.rollback();
      log.error(e.getMessage(), e);
    } 
  }

我們先來看看上述這種寫法的幾種問題

然后我們結(jié)合上述寫法店读,它會(huì)在判斷批處理?xiàng)l數(shù)達(dá)到1000條的時(shí)候會(huì)去手動(dòng)commit,然后又手動(dòng)clearCache攀芯,我們先來看看commit到底都做了一些什么屯断,以下為調(diào)用鏈

@Override
public void commit() {
    commit(false);
}  

@Override
public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

private boolean isCommitOrRollbackRequired(boolean force) {
// autoCommit默認(rèn)為false,調(diào)用過插入侣诺、更新裹纳、刪除之后的dirty值為true
    return (!autoCommit && dirty) || force;
}

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

我們會(huì)發(fā)現(xiàn),其實(shí)你直接調(diào)用commit的情況下紧武,它就已經(jīng)做了clearLocalCache這件事情剃氧,所以大可不必在commit后加上一句clearCache

flushStatements的作用就是將前面所有執(zhí)行過的INSERT、UPDATE阻星、DELETE語句真正刷新到數(shù)據(jù)庫中朋鞍。底層調(diào)用了JDBCstatement.executeBatch方法。

這個(gè)方法的返回值通俗來說如果執(zhí)行的是同一個(gè)方法并且執(zhí)行的是同一條SQL妥箕,注意這里的SQL還沒有設(shè)置參數(shù)滥酥,也就是說SQL里的占位符?還沒有被處理成真正的參數(shù),那么每次執(zhí)行的結(jié)果共用一個(gè)BatchResult畦幢,真正的結(jié)果可以通過BatchResult中的getUpdateCounts方法獲取坎吻。

另外如果執(zhí)行了SELECT操作,那么會(huì)將先前的UPDATE宇葱、INSERT瘦真、DELETE語句刷新到數(shù)據(jù)庫中。這一點(diǎn)去看BatchExecutor中的doQuery方法即可黍瞧。

1.4 Mybatis升級(jí)使用

由于項(xiàng)目中用到批處理的地方肯定不止一個(gè)诸尽,那每用一次就需要CV一下,能不能一勞永逸

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.function.ToIntFunction;

@Slf4j
@Component
public class MybatisBatchUtils {

    /**
     * 每次處理1000條
     */
    private static final int BATCH = 1000;

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    /**
     * 批量處理修改或者插入
     *
     * @param data     需要被處理的數(shù)據(jù)
     * @param function 自定義處理邏輯
     * @return int 影響的總行數(shù)
     */
    public  <T> int batchUpdateOrInsert(List<T> data, ToIntFunction<T> function) {
        int count = 0;
        SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            for (int index = 0; index < data.size(); index++) {
                count += function.applyAsInt(data.get(index));
                if (index != 0 && index % BATCH == 0) {
                    batchSqlSession.flushStatements();
                }
            }
            batchSqlSession.commit();
        } catch (Exception e) {
            batchSqlSession.rollback();
            log.error(e.getMessage(), e);
        } finally {
            batchSqlSession.close();
        }
        return count;
    }
}

偽代碼使用案例

@Resource
private 某Mapper類 mapper實(shí)例對(duì)象;

batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, item -> mapper實(shí)例對(duì)象.insert方法(item));

1.5 Mybatis批量標(biāo)準(zhǔn)寫法

我們知道上面我們提到了BatchExecutor執(zhí)行器印颤,我們知道每個(gè)SqlSession都會(huì)擁有一個(gè)Executor對(duì)象您机,這個(gè)對(duì)象才是執(zhí)行 SQL 語句的幕后黑手,我們也知道SpringMybatis整合的時(shí)候使用的SqlSessionSqlSessionTemplate,默認(rèn)用的是ExecutorType.SIMPLE际看,這個(gè)時(shí)候你通過自動(dòng)注入獲得的Mapper對(duì)象其實(shí)是沒有開啟批處理的

那么我們實(shí)際上是需要通過sqlSessionFactory.openSession(ExecutorType.BATCH)得到的sqlSession對(duì)象(此時(shí)里面的ExecutorBatchExecutor)去獲得一個(gè)新的Mapper對(duì)象才能生效

所以更改一下這個(gè)通用的方法咸产,把MapperClass也一塊傳遞進(jìn)來

public class MybatisBatchUtils {
    
    /**
    * 每次處理1000條
    */
    private static final int BATCH_SIZE = 1000;
    
    @Resource
    private SqlSessionFactory sqlSessionFactory;
    
    /**
    * 批量處理修改或者插入
    *
    * @param data     需要被處理的數(shù)據(jù)
    * @param mapperClass  Mybatis的Mapper類
    * @param function 自定義處理邏輯
    * @return int 影響的總行數(shù)
    */
    public  <T,U,R> int batchUpdateOrInsert(List<T> data, Class<U> mapperClass, BiFunction<T,U,R> function) {
        int i = 1;
        SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            U mapper = batchSqlSession.getMapper(mapperClass);
            int size = data.size();
            for (T element : data) {
                function.apply(element,mapper);
                if ((i % BATCH_SIZE == 0) || i == size) {
                    batchSqlSession.flushStatements();
                }
                i++;
            }
            // 非事務(wù)環(huán)境下強(qiáng)制commit,事務(wù)情況下該commit相當(dāng)于無效
            batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
        } catch (Exception e) {
            batchSqlSession.rollback();
            throw new CustomException(e);
        } finally {
            batchSqlSession.close();
        }
        return i - 1;
    }
}

這里會(huì)判斷是否是事務(wù)環(huán)境仲闽,不是的話會(huì)強(qiáng)制提交锐朴,如果是事務(wù)環(huán)境的話,這個(gè)commit設(shè)置force值是無效的

使用案例:

batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, xxxxx.class, (item, mapper實(shí)例對(duì)象) -> mapper實(shí)例對(duì)象.insert方法(item));

1.6 使用rewriteBatchedStatements

rewriteBatchedStatements=trueMyBatis 配置文件中的一個(gè)屬性蔼囊,用于優(yōu)化批量插入或更新操作的性能。
當(dāng) rewriteBatchedStatements=true 時(shí)衣迷,MyBatis 會(huì)將批量插入或更新的 SQL 語句重寫為單條語句畏鼓,然后發(fā)送給數(shù)據(jù)庫執(zhí)行。這樣可以減少網(wǎng)絡(luò)傳輸?shù)拇螖?shù)壶谒,提高插入或更新操作的效率云矫。

默認(rèn)情況下,rewriteBatchedStatements 的值為 false汗菜,即不啟用該優(yōu)化让禀。這是因?yàn)樵谀承?shù)據(jù)庫(如 MySQL)中,啟用該優(yōu)化可能會(huì)導(dǎo)致主鍵沖突或其他異常陨界。因此巡揍,如果使用的數(shù)據(jù)庫支持該優(yōu)化并且不會(huì)引起問題,可以將該屬性設(shè)置為 true菌瘪,以提高批量插入或更新操作的性能腮敌。

需要注意的是,不是所有的數(shù)據(jù)庫和驅(qū)動(dòng)程序都支持 rewriteBatchedStatements俏扩。因此糜工,在配置該屬性之前,應(yīng)該先了解你所使用的數(shù)據(jù)庫的特性和限制录淡。

具體配置如下:

spring:
  application:
    name: batch-demo
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: xxxx
    password: xxxx
    url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&serverTimezone=GMT&characterEncoding=utf8&rewriteBatchedStatements=true
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捌木,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嫉戚,更是在濱河造成了極大的恐慌刨裆,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彬檀,死亡現(xiàn)場(chǎng)離奇詭異崔拥,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)凤覆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門链瓦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事慈俯〔橙校” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵贴膘,是天一觀的道長(zhǎng)卖子。 經(jīng)常有香客問我,道長(zhǎng)刑峡,這世上最難降的妖魔是什么洋闽? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮突梦,結(jié)果婚禮上诫舅,老公的妹妹穿的比我還像新娘。我一直安慰自己宫患,他們只是感情好刊懈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著娃闲,像睡著了一般虚汛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上皇帮,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天卷哩,我揣著相機(jī)與錄音,去河邊找鬼属拾。 笑死殉疼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捌年。 我是一名探鬼主播瓢娜,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼礼预!你這毒婦竟也來了眠砾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤托酸,失蹤者是張志新(化名)和其女友劉穎褒颈,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體励堡,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谷丸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了应结。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刨疼。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泉唁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出揩慕,到底是詐尸還是另有隱情亭畜,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布迎卤,位于F島的核電站拴鸵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蜗搔。R本人自食惡果不足惜劲藐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望樟凄。 院中可真熱鬧聘芜,春花似錦、人聲如沸不同。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽二拐。三九已至,卻和暖如春凳兵,著一層夾襖步出監(jiān)牢的瞬間百新,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工庐扫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饭望,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓形庭,卻偏偏與公主長(zhǎng)得像铅辞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子萨醒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容