Mybatis-Plus通過SQL註入器實現批量插入的實踐

前言

批量插入是實際工作中常見的一個功能,mysql支持一條sql語句插入多條數據。但是Mybatis-Plus中默認提供的saveBatch方法並不是真正的批量插入,而是遍歷實體集合每執行一次insert語句插入一條記錄。相比批量插入,性能上顯然會差很多。
今天談一下,在Mybatis-Plus中如何通過SQL註入器實現真正的批量插入。

一、mysql批量插入的支持

insert批量插入的語法支持:

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');

二、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("[email protected]");
            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("[email protected]");
            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("[email protected]");
            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("[email protected]");
            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註入器批量插入內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: