Mybatis Plus 批量 Insert_新增數(shù)據(jù)(圖文講解)
前言
大家好,我是小哈。
本小節(jié)中,我們將學(xué)習(xí)如何通過 Mybatis Plus 實現(xiàn) MySQL 批量插入數(shù)據(jù)。
什么是批量插入?優(yōu)勢在哪里?
先拋出一個問題:假設(shè)老板給你下了個任務(wù),向數(shù)據(jù)庫中添加 100 萬條數(shù)據(jù),并且不能耗時太久!
通常來說,我們向 MySQL 中新增一條記錄,SQL 語句類似如下:
INSERT INTO `t_user` (`name`, `age`, `gender`) VALUES ('犬小哈0', 0, 1);
如果你需要添加 100 萬條數(shù)據(jù),就需要多次執(zhí)行此語句,這就意味著頻繁地與數(shù)據(jù)庫建立鏈接,必然導(dǎo)致網(wǎng)絡(luò) IO 開銷巨大,并且每一次數(shù)據(jù)庫執(zhí)行 SQL 都需要進行解析、優(yōu)化等操作。
幸運的是,MySQL 支持一條 SQL 語句可以批量插入多條記錄,格式如下:
INSERT INTO `t_user` (`name`, `age`, `gender`) VALUES ('犬小哈0', 0, 1), ('犬小哈1', 0, 1), ('犬小哈3', 0, 1);
和常規(guī)的?INSERT
?語句不同的是,VALUES
?支持多條記錄,通過?,
?逗號隔開。這樣,可以實現(xiàn)一次性插入多條記錄。
數(shù)據(jù)量不多的情況下,常規(guī)?INSERT
?和批量插入性能差距不大,但是,一旦數(shù)量級上去后,執(zhí)行耗時差距就拉開了,在后面我們會實測一下它們之間的耗時對比。
表與實體類
先創(chuàng)建一個測試表?t_user
, 執(zhí)行腳本如下:
DROP TABLE IF EXISTS user; CREATE TABLE `t_user` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵ID', `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名', `age` int(11) NULL DEFAULT NULL COMMENT '年齡', `gender` tinyint(2) NOT NULL DEFAULT 0 COMMENT '性別,0:女 1:男', PRIMARY KEY (`id`) ) COMMENT = '用戶表';
再定義一個名為?User
?實體類:
/**
* @author: 犬小哈
* @from: 公眾號:小哈學(xué)Java, 網(wǎng)站:www.quanxiaoha.com
* @date: 2022-12-13 14:13
* @version: v1.0.0 * @description: TODO **/ @Data @TableName("t_user") public class User { /** * 主鍵 ID, @TableId 注解定義字段為表的主鍵,type 表示主鍵類型,IdType.AUTO 表示隨著數(shù)據(jù)庫 ID 自增 */ @TableId(type = IdType.AUTO) private Long id; /** * 姓名 */ private String name; /** * 年齡 */ private Integer age; /** * 性別 */ private Integer gender; }
TIP:?
@Data
?是 Lombok 注解,偷懶用的,加上它即可免寫繁雜的?getXXX/setXXX
?相關(guān)方法,不了解的小伙伴可自行搜索一下如何使用。
Mybatis Plus 偽批量插入
在前面《新增數(shù)據(jù)》?小節(jié)中,我們已經(jīng)知道了 Mybatis Plus 內(nèi)部封裝的批量插入?savaBatch()
?是個假的批量插入,示例代碼如下:
List<User> users =new ArrayList<>();
for (int i = 0; i < 5; i++) { User user = new User(); user.setName("犬小哈" + i); user.setAge(i); user.setGender(1); users.add(user); } // 批量插入 boolean isSuccess = userService.saveBatch(users); System.out.println("isSuccess:" + isSuccess);
通過打印實際執(zhí)行 SQL , 我們發(fā)現(xiàn)還是一條一條的執(zhí)行?INSERT
:
并且還帶著大家看了內(nèi)部實現(xiàn)的源碼,這種方式比起自己?for
?循環(huán)一條一條?INSERT
?插入數(shù)據(jù)性能要更高,原因是在會話這塊做了優(yōu)化,雖然實際執(zhí)行并不是真的批量插入。
利用 SQL 注入器實現(xiàn)真的批量插入
接下來,小哈就手把手帶你通過 Mybatis Plus 框架的 SQL 注入器實現(xiàn)一個真的批量插入。
示例項目結(jié)構(gòu)
先貼一張示例項目的結(jié)構(gòu):
注意看我紅線標注的部分,主要關(guān)注這 4 個類與接口。
新建批量插入 SQL 注入器
在工程?config
?目錄下創(chuàng)建一個 SQL 注入器?InsertBatchSqlInjector
?:
/**
* @author: 犬小哈
* @from: 公眾號:小哈學(xué)Java, 網(wǎng)站:www.quanxiaoha.com
* @date: 2023-01-05 14:42
* @version: v1.0.0 * @description: 批量插入 SQL 注入器 **/ public class InsertBatchSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { // super.getMethodList() 保留 Mybatis Plus 自帶的方法 List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); // 添加自定義方法:批量插入,方法名為 insertBatchSomeColumn methodList.add(new InsertBatchSomeColumn()); return methodList; } }
說說 InsertBatchSomeColumn
InsertBatchSomeColumn
?是 Mybatis Plus 內(nèi)部提供的默認批量插入,只不過這個方法作者只在 MySQL 數(shù)據(jù)測試過,所以沒有將它作為通用方法供外部調(diào)用,注意看注釋:
源碼復(fù)制出來,如下:
/**
* 批量新增數(shù)據(jù),自選字段 insert
* <p> 不同的數(shù)據(jù)庫支持度不一樣!!! 只在 mysql 下測試過!!! 只在 mysql 下測試過!!! 只在 mysql 下測試過!!! </p>
* <p> 除了主鍵是 <strong> 數(shù)據(jù)庫自增的未測試 </strong> 外理論上都可以使用!!! </p>
* <p> 如果你使用自增有報錯或主鍵值無法回寫到entity,就不要跑來問為什么了,因為我也不知道!!! </p>
* <p>
* 自己的通用 mapper 如下使用:
* <pre>
* int insertBatchSomeColumn(List<T> entityList);
* </pre>
* </p>
*
* <li> 注意: 這是自選字段 insert !!,如果個別字段在 entity 里為 null 但是數(shù)據(jù)庫中有配置默認值, insert 后數(shù)據(jù)庫字段是為 null 而不是默認值 </li>
*
* <p>
* 常用的 {@link Predicate}:
* </p>
*
* <li> 例1: t -> !t.isLogicDelete() , 表示不要邏輯刪除字段 </li>
* <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名為 version 的字段 </li>
* <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略為 UPDATE 的字段 </li>
*
* @author miemie
* @since 2018-11-29
*/
@SuppressWarnings("serial") public class InsertBatchSomeColumn extends AbstractMethod { /** * 字段篩選條件 */ @Setter @Accessors(chain = true) private Predicate<TableFieldInfo> predicate; /** * 默認方法名 */ public InsertBatchSomeColumn() { // 方法名 super("insertBatchSomeColumn"); } /** * 默認方法名 * * @param predicate 字段篩選條件 */ public InsertBatchSomeColumn(Predicate<TableFieldInfo> predicate) { super("insertBatchSomeColumn"); this.predicate = predicate; } /** * @param name 方法名 * @param predicate 字段篩選條件 * @since 3.5.0 */ public InsertBatchSomeColumn(String name, Predicate<TableFieldInfo> predicate) { super(name); this.predicate = predicate; } @SuppressWarnings("Duplicates") @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; SqlMethod sqlMethod = SqlMethod.INSERT_ONE; List<TableFieldInfo> fieldList = tableInfo.getFieldList(); String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) + this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY); String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET; String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) + this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY); insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET; String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA); String keyProperty = null; String keyColumn = null; // 表包含主鍵處理邏輯,如果不包含主鍵當普通字段處理 if (tableInfo.havePK()) { if (tableInfo.getIdType() == IdType.AUTO) { /* 自增主鍵 */ keyGenerator = Jdbc3KeyGenerator.INSTANCE; keyProperty = tableInfo.getKeyProperty(); keyColumn = tableInfo.getKeyColumn(); } else { if (null != tableInfo.getKeySequence()) { keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); keyProperty = tableInfo.getKeyProperty(); keyColumn = tableInfo.getKeyColumn(); } } } String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn); } }
配置 SQL 注入器
在?config
?包下創(chuàng)建?MybatisPlusConfig
?配置類:
/**
* @Author: 犬小哈
* @From: 公眾號:小哈學(xué)Java, 網(wǎng)站:www.quanxiaoha.com
* @Date: 2022-12-15 18:29
* @Version: v1.0.0 * @Description: TODO **/ @Configuration @MapperScan("com.quanxiaoha.mybatisplusdemo.mapper") public class MybatisPlusConfig { /** * 自定義批量插入 SQL 注入器 */ @Bean public InsertBatchSqlInjector insertBatchSqlInjector() { return new InsertBatchSqlInjector(); } }
新建 MyBaseMapper
在?config
?包下創(chuàng)建?MyBaseMapper
?接口,讓其繼承自 Mybatis Plus 提供的?BaseMapper
, 并定義批量插入方法:
/**
* @author: 犬小哈
* @from: 公眾號:小哈學(xué)Java, 網(wǎng)站:www.quanxiaoha.com
* @date: 2022-12-13 14:13
* @version: v1.0.0 * @description: TODO **/ public interface MyBaseMapper<T> extends BaseMapper<T> { // 批量插入 int insertBatchSomeColumn(@Param("list") List<T> batchList); }
注意:方法名必須為?
insertBatchSomeColumn
, 和?InsertBatchSomeColumn
?內(nèi)部定義好的方法名保持一致。
新建 UserMapper
在?mapper
?包下創(chuàng)建?UserMapper
?接口,注意繼承剛剛自定義的?MyBaseMapper
, 而不是?BaseMapper
?:
/**
* @author: 犬小哈
* @from: 公眾號:小哈學(xué)Java, 網(wǎng)站:www.quanxiaoha.com
* @date: 2022-12-13 14:13
* @version: v1.0.0 * @description: TODO **/ public interface UserMapper extends MyBaseMapper<User> { }
測試批量插入
完成上面這些工作后,就可以使用 Mybatis Plus 提供的批量插入功能了。我們新建一個單元測試,并注入?UserMapper
?:
@Autowired
private UserMapper userMapper;
單元測試如下:
@Test
void testInsertBatch() {
List<User> users = new ArrayList<>(); for (int i = 0; i < 3; i++) { User user = new User(); user.setName("犬小哈" + i); user.setAge(i); user.setGender(1); users.add(user); } userMapper.insertBatchSomeColumn(users); }
控制臺實際執(zhí)行 SQL 如下:
可以看到這次是真實的批量插入了,舒服了~
性能對比
我們來測試一下插入 105000 條數(shù)據(jù),分別使用?for
?循環(huán)插入數(shù)據(jù)、savaBatch()
?偽批量插入、與真實批量插入三種模式,看看耗時差距多少。
小哈這里的機器配置如下:
for
?循環(huán)插入
單元測試代碼如下:
@Test
void testInsert1() {
// 總耗時:722963 ms, 約 12 分鐘 long start = System.currentTimeMillis(); for (int i = 0; i < 105000; i++) { User user = new User(); user.setName("犬小哈" + i); user.setAge(i); user.setGender(1); userMapper.insert(user); } System.out.println(String.format("總耗時:%s ms", System.currentTimeMillis() - start)); }
savaBatch()
?偽批量插入
單元測試代碼如下:
@Test
void testInsert2() {
// 總耗時:95864 ms, 約一分鐘30秒左右 long start = System.currentTimeMillis(); List<User> users = new ArrayList<>(); for (int i = 0; i < 105000; i++) { User user = new User(); user.setName("犬小哈" + i); user.setAge(i); user.setGender(1); users.add(user); } userService.saveBatch(users); System.out.println(String.format("總耗時:%s ms", System.currentTimeMillis() - start)); }
真實批量插入
注意,真實業(yè)務(wù)場景下,也不可能會將 10 萬多條記錄組裝成一條 SQL 進行批量插入,因為數(shù)據(jù)庫對執(zhí)行 SQL 大小是有限制的(這個數(shù)值可以自行設(shè)置),還是需要分片插入,比如取 1000 條執(zhí)行一次批量插入,單元測試代碼如下:
@Test
void testInsertBatch1() {
// 總耗時:6320 ms, 約 6 秒 long start = System.currentTimeMillis(); List<User> users = new ArrayList<>(); for (int i = 0; i < 1006; i++) { User user = new User(); user.setName("犬小哈" + i); user.setAge(i); user.setGender(1); users.add(user); } // 分片插入(每 1000 條執(zhí)行一次批量插入) int batchSize = 1000; int total = users.size(); // 需要執(zhí)行的次數(shù) int insertTimes = total / batchSize; // 最后一次執(zhí)行需要提交的記錄數(shù)(防止可能不足 1000 條) int lastSize = batchSize; if (total % batchSize != 0) { insertTimes++; lastSize = total%batchSize; } for (int j = 0; j < insertTimes; j++) { if (insertTimes == j+1) { batchSize = lastSize; } // 分片執(zhí)行批量插入 userMapper.insertBatchSomeColumn(users.subList(j*batchSize, (j*batchSize+batchSize))); } System.out.println(String.format("總耗時:%s ms", System.currentTimeMillis() - start)); }
耗時對比
方式 | 總耗時 |
---|---|
for ?循環(huán)插入 |
722963 ms, 約 12 分鐘 |
savaBatch() ?偽批量插入 |
95864 ms, 約一分鐘30秒左右 |
真實批量插入 | 6320 ms, 約 6 秒 |
耗時對比非常直觀,在大批量數(shù)據(jù)新增的場景下,批量插入性能最高。文章來源:http://www.zghlxwxcb.cn/news/detail-767676.html
結(jié)語
本小節(jié)中,我們學(xué)習(xí)了如何通過 Mybatis Plus 的 SQL 注入器實現(xiàn)真實的批量插入,同時最后還對比了三種不同方式插入 10 萬多數(shù)據(jù)的耗時,很直觀的看到在海量數(shù)據(jù)場景下,批量插入的性能是最強的。文章來源地址http://www.zghlxwxcb.cn/news/detail-767676.html
到了這里,關(guān)于spring boot集成mybatis-plus——Mybatis Plus 批量 Insert_新增數(shù)據(jù)(圖文講解)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!