首頁 > 軟體

Mybatis-Plus通過SQL隱碼攻擊器實現批次插入的實踐

2022-08-11 14:02:45

前言

批次插入是實際工作中常見的一個功能,mysql支援一條sql語句插入多條資料。但是Mybatis-Plus中預設提供的saveBatch方法並不是真正的批次插入,而是遍歷實體集合每執行一次insert語句插入一條記錄。相比批次插入,效能上顯然會差很多。
今天談一下,在Mybatis-Plus中如何通過SQL隱碼攻擊器實現真正的批次插入。

一、mysql批次插入的支援

insert批次插入的語法支援:

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

二、Mybatis-Plus預設saveBatch方法解析

1、測試工程建立

測試的資料表:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主鍵ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年齡',
  `email` varchar(50) DEFAULT NULL COMMENT '郵箱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

在IDEA中設定好資料庫連線,並安裝好MybatisX-Generator外掛,生成對應表的model、mapper、service、xml檔案。

生成的檔案推薦儲存在工程目錄下,generator目錄下。先生成檔案,使用者根據自己的需要,再將檔案移動到指定目錄,這樣避免出現檔案覆蓋。

生成實體的設定選項,這裡我勾選了Lombok和Mybatis-Plus3,生成的類更加優雅。

移動生成的檔案到對應目錄:

由於都是生成的程式碼,這裡就不補充程式碼了。

2、預設批次插入saveBatch方法測試

    @Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        userService.saveBatch(list);
    }

執行紀錄檔:

顯然,這裡每次執行insert操作,都只插入了一條資料。

3、saveBatch方法實現分析

//批次儲存的方法,做了分批請求處理,預設一次處理1000條資料
default boolean saveBatch(Collection<T> entityList) {
    return this.saveBatch(entityList, 1000);
}

//使用者也可以自己指定每批次處理的請求數量
boolean saveBatch(Collection<T> entityList, int batchSize);
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
    Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);
    return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {
        int size = list.size();
        int idxLimit = Math.min(batchSize, size);
        int i = 1;

        for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) {
            E element = var7.next();
            consumer.accept(sqlSession, element);
            //每次達到批次數,sqlSession就重新整理一次,進行資料庫請求,生成Id
            if (i == idxLimit) {
                sqlSession.flushStatements();
                idxLimit = Math.min(idxLimit + batchSize, size);
            }
        }

    });
}

我們將批次數設定為3,用來測試executeBatch的處理機制。

    @Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        //批次數設為3,用來測試
        userService.saveBatch(list,3);
    }

執行結果,首批提交的請求,已經生成了id,還沒有提交的id為null。
(這裡的提交是sql請求,而不是說的事物提交)

小結:
Mybatis-Plus中預設的批次儲存方法saveBatch,底層是通過sqlSession.flushStatements()將一個個單條插入的insert語句分批次進行提交。
相比遍歷集合去呼叫userMapper.insert(entity),執行一次提交一次,saveBatch批次儲存有一定的效能提升,但從sql層面上來說,並不算是真正的批次插入。

補充:

遍歷集合單次提交的批次插入。

 @Test
    public void forEachInsert() {
        System.out.println("forEachInsert 插入開始========");
        long start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            userMapper.insert(list.get(i));
        }
        System.out.println("foreach 插入耗時:"+(System.currentTimeMillis()-start));
    }

三、Mybatis-plus中SQL隱碼攻擊器介紹

SQL隱碼攻擊器官方檔案:https://baomidou.com/pages/42ea4a/

1.sqlInjector介紹

SQL隱碼攻擊器sqlInjector 用於注入 ISqlInjector 介面的子類,實現自定義方法注入。
參考預設注入器 DefaultSqlInjector

Mybatis-plus預設可以注入的方法如下,大家也可以參考其實現自己擴充套件:

預設注入器DefaultSqlInjector的內容:

public class DefaultSqlInjector extends AbstractSqlInjector {
    public DefaultSqlInjector() {
    }

    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //注入通用的dao層介面的操作方法
        return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectOne(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());
    }
}

2.擴充套件中提供的4個可注入方法實現

目前在mybatis-plus的擴充套件外掛中com.baomidou.mybatisplus.extension,給我們額外提供了4個注入方法。

  • AlwaysUpdateSomeColumnById 根據Id更新每一個欄位,全量更新不忽略null欄位,解決mybatis-plus中updateById預設會自動忽略實體中null值欄位不去更新的問題。
  • InsertBatchSomeColumn 真實批次插入,通過單SQL的insert語句實現批次插入
  • DeleteByIdWithFill 帶自動填充的邏輯刪除,比如自動填充更新時間、操作人
  • Upsert 更新or插入,根據唯一約束判斷是執行更新還是刪除,相當於提供insert on duplicate key update支援
insert into t_name (uid, app_id,createTime,modifyTime)
values(111, 1000000,'2017-03-07 10:19:12','2017-03-07 10:19:12')
on duplicate key update uid=111, app_id=1000000, 
createTime='2017-03-07 10:19:12',modifyTime='2017-05-07 10:19:12'

mysql在存在主鍵衝突或者唯一鍵衝突的情況下,根據插入策略不同,一般有以下三種避免方法。

  • insert ignore
  • replace into
  • insert on duplicate key update

這裡不展開介紹,大家可以自行檢視:https://www.jb51.net/article/194579.htm

四、通過SQL隱碼攻擊器實現真正的批次插入

通過SQL隱碼攻擊器sqlInjector 增加批次插入方法InsertBatchSomeColumn的過程如下:

1.繼承DefaultSqlInjector擴充套件自定義的SQL隱碼攻擊器

程式碼如下:

/**
 * 自定義Sql注入
 */
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
         //更新時自動填充的欄位,不用插入值
         methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

2.將自定義的SQL隱碼攻擊器注入到Mybatis容器中

程式碼如下:

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MySqlInjector sqlInjector() {
        return new MySqlInjector();
    }
}

3.繼承 BaseMapper 新增自定義方法

public interface CommonMapper<T> extends BaseMapper<T> {
    /**
     * 全量插入,等價於insert
     * @param entityList
     * @return
     */
    int insertBatchSomeColumn(List<T> entityList);
}

4.Mapper層介面繼承新的CommonMapper

public interface UserMapper extends CommonMapper<User> {

}

5.單元測試

 @Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        userMapper.insertBatchSomeColumn(list);
    }

執行結果:

可以看到已經實現單條insert語句支援資料的批次插入。

注意⚠️:
預設的insertBatchSomeColumn實現中,並沒有類似saveBatch中的分配提交處理,
這就存在一個問題,如果出現一個非常大的集合,就會導致最後組裝提交的insert語句的長度超過mysql的限制。

6.insertBatchSomeColumn新增分批次處理機制

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private UserMapper userMapper;

    /**
     * 採用insertBatchSomeColumn重寫saveBatch方法,保留分批次處理機制
     * @param entityList
     * @param batchSize
     * @return
     */
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public boolean saveBatch(Collection<User> entityList, int batchSize) {
        try {
            int size = entityList.size();
            int idxLimit = Math.min(batchSize, size);
            int i = 1;
            //儲存單批提交的資料集合
            List<User> oneBatchList = new ArrayList<>();
            for(Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) {
                User element = var7.next();
                oneBatchList.add(element);
                if (i == idxLimit) {
                    userMapper.insertBatchSomeColumn(oneBatchList);
                    //每次提交後需要清空集合資料
                    oneBatchList.clear();
                    idxLimit = Math.min(idxLimit + batchSize, size);
                }
            }
        }catch (Exception e){
            log.error("saveBatch fail",e);
            return false;
        }
        return  true;
    }

更好的實現是繼承ServiceImpl實現類,自己擴充套件通用的服務實現類,在其中重寫通用的saveBatch方法,這樣就不用在每一個服務類中都重寫一遍saveBatch方法。

單元測試:

    @Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        //批次數設為3,用來測試
        userService.saveBatch(list,3);
    }

執行結果:

分4次採用insert批次新增,符合我們的結果預期。

總結

本文主要介紹了Mybatis-Plus中如何通過SQL隱碼攻擊器實現真正的批次插入。主要掌握如下內容:
1、瞭解Mybatis-Plus中SQL隱碼攻擊器有什麼作用,如何去進行擴充套件。
2、預設的4個擴充套件方法各自的作用。
3、預設的saveBatch批次新增和通過insertBatchSomeColumn實現的批次新增的底層實現原理的區別,為什麼insertBatchSomeColumn效能更好以及存在哪些弊端。
4、為insertBatchSomeColumn新增分批次處理機制,避免批次插入的insert語句過長問題。

到此這篇關於Mybatis-Plus通過SQL隱碼攻擊器實現批次插入的實踐的文章就介紹到這了,更多相關Mybatis-Plus SQL隱碼攻擊器批次插入內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com