MyBatis批量插入的主流方案主要有以下:
序号 | 方案 | 优点 | 缺点 | 使用情形 |
---|---|---|---|---|
1 | for循环单条数据依次插入 | 1. 代码简单 2. 容易实现和理解 |
1. 性能较差,因每次插入都需要一次数据库连接和提交 2. 对数据库负载大 |
适用于数据量较少且对性能要求不高的场景 |
2 | 在Mapper.xml的insert使用foreach,循环list生成大型insert一次执行 | 1. 性能较好,减少了数据库连接和提交的次数 2. 实现较为简单 |
1. SQL语句长度受限,可能会因为SQL语句过长导致失败 2. 需要注意参数数量和数据库的限制 |
适用于数据量适中,SQL长度在可控范围内的场景 |
3 | 使用SqlSession手动控制事务,一次性提交大量的insert | 1. 性能最佳,减少了大量数据库连接和提交次数 2. 可以处理大批量数据 |
1. 实现较为复杂,需要手动管理事务 2. 如果事务中有错误,处理较为麻烦 |
适用于数据量大,对性能要求高,并且能接受较复杂实现的场景 |
第1种方案效率太低,通常不会使用;
第2种方案在数据量太大时生成的sql会超出mysql的最大允许包大小(max_allowed_packet)导致失败,通常会采取分批次执行
第3种方案比较合适,但是每一处使用批量插入都要手写一堆代码来处理吗?有没有更合适的方式来实现呢?
需要解决的问题:
每一个Entity在Mapper中的实现都有基础的增删查改,如何去复用这里的“增”来实现我们的批量新增呢?
注解 ,我们只要加上一个注解标注出来就可以了
说到效率大部分人都会想到多线程,理论上在insert时会锁表,那么我们使用多线程进行插入时,不同线程之间发生冲突会不会造成线程等待,导致多线程不起效果呢?我觉得还是得要实践才能出真知。
暂时不考虑
注解类:
/**
* 描述:
*
* @author : zzq
* @date : 2024-06-02 12:36
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BatchInsert {
}
import com.beust.jcommander.ParameterException;
import com.ruoyi.common.utils.StringUtils;
import com.xxl.job.executor.core.config.XxlJobSampleConfig;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* 描述: 批量插入工具类
* 使用@BatchInsert注解进行批量插入
* 使用方法:
* 1. 使用@BatchInsert标记Mapper接口中的insert方法
* [若使用分表,请确保分表字段在参数中,并且作为第二个参数传入,例如
* /@BatchInsert
* public int insertNwForecastDataByTableName(@Param("nwForecastData") NwForecastData nwForecastData, @Param("tableName") String tableName);]
* 2. 使用工具类进行批量插入
*
* @author : zzq
* @date : 2024-06-02 12:35
**/
public class BatchInsertUtil {
private static final Logger logger = LoggerFactory.getLogger(BatchInsertUtil.class);
private static final int THREAD_COUNT = Runtime.getRuntime().availableProcessors();
public static final int BATCH_INSERT_SIZE = 3000;
private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
/**
* 批量插入
* @param list 待插入列表
* @param mapperType Mapper类
*/
public static <T> void batchInsert(List<T> list, Class<?> mapperType) {
batchInsert(list, mapperType, null);
}
/**
* 批量插入
* @param list 待插入列表
* @param mapperType Mapper类
* @param tableName 分表名
*/
public static <T> void batchInsert(List<T> list, Class<?> mapperType, String tableName) {
SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
Object mapper = sqlSession.getMapper(mapperType);
Method method = findBatchInsertMethod(mapperType);
if (method != null) {
performBatchInsert(sqlSession, mapper, method, list, tableName);
}
} catch (Exception e) {
logger.error("批量插入失败", e);
}
}
private static Method findBatchInsertMethod(Class<?> mapperType) {
for (Method method : mapperType.getMethods()) {
if (method.isAnnotationPresent(BatchInsert.class)) {
return method;
}
}
return null;
}
private static <T> void performBatchInsert(SqlSession sqlSession, Object mapper, Method method, List<T> list, String tableName) throws Exception {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
insertWithSingleParameter(sqlSession, mapper, method, list);
} else if (parameterTypes.length == 2) {
if (StringUtils.isBlank(tableName)) {
throw new ParameterException("Table name cannot be empty");
}
insertWithTwoParameters(sqlSession, mapper, method, list, tableName);
} else {
throw new IllegalArgumentException("Method " + method.getName() + " does not have the correct parameter count for batch insert");
}
}
private static <T> void insertWithSingleParameter(SqlSession sqlSession, Object mapper, Method method, List<T> list) throws Exception {
int count = 0;
for (T item : list) {
method.invoke(mapper, item);
count++;
if (count % BATCH_INSERT_SIZE == 0) {
sqlSession.commit();
}
}
if (count % BATCH_INSERT_SIZE != 0) {
sqlSession.commit();
}
}
private static <T> void insertWithTwoParameters(SqlSession sqlSession, Object mapper, Method method, List<T> list, String tableName) throws Exception {
int count = 0;
for (T item : list) {
method.invoke(mapper, item, tableName);
count++;
if (count % BATCH_INSERT_SIZE == 0) {
sqlSession.commit();
}
}
if (count % BATCH_INSERT_SIZE != 0) {
sqlSession.commit();
}
}
/**
* 多线程同步批量插入
* @param list 待插入列表
* @param mapperType Mapper类
*/
public static <T> void syncBatchInsert(List<T> list, Class<?> mapperType) {
syncBatchInsert(list, mapperType, null);
}
/**
* 多线程同步批量插入
* @param list 待插入列表
* @param mapperType Mapper类
* @param tableName 分表名
*/
public static <T> void syncBatchInsert(List<T> list, Class<?> mapperType, String tableName) {
SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
logger.info("可用线程数:" + THREAD_COUNT);
List<Future<Void>> futures = new ArrayList<>();
int batchCount = (list.size() + BATCH_INSERT_SIZE - 1) / BATCH_INSERT_SIZE;
for (int i = 0; i < batchCount; i++) {
int start = i * BATCH_INSERT_SIZE;
int end = Math.min(start + BATCH_INSERT_SIZE, list.size());
List<T> sublist = list.subList(start, end);
Future<Void> future = executorService.submit(() -> {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
Object mapper = sqlSession.getMapper(mapperType);
Method method = findBatchInsertMethod(mapperType);
if (method != null) {
performBatchInsert(sqlSession, mapper, method, sublist, tableName);
}
} catch (Exception e) {
logger.error("同步批量插入失败", e);
}
return null;
});
futures.add(future);
}
waitForAll(futures);
}
/**
* 多线程异步批量插入
* @param list 待插入列表
* @param mapperType Mapper类
*/
public static <T> List<Future<Void>> asyncBatchInsert(List<T> list, Class<?> mapperType) {
return asyncBatchInsert(list, mapperType, null);
}
/**
* 多线程异步批量插入
* @param list 待插入列表
* @param mapperType Mapper类
* @param tableName 分表名
*/
public static <T> List<Future<Void>> asyncBatchInsert(List<T> list, Class<?> mapperType, String tableName) {
int size = list.size();
List<Future<Void>> futures = new ArrayList<>();
for (int i = 0; i <= size / BATCH_INSERT_SIZE; i++) {
int startIndex = i * BATCH_INSERT_SIZE;
int endIndex = Math.min(startIndex + BATCH_INSERT_SIZE, size);
List<T> subList = new CopyOnWriteArrayList<>(list.subList(startIndex, endIndex));
futures.addAll(executeBatchInsert(subList, mapperType, tableName));
}
list.clear();
return futures;
}
private static List<Future<Void>> executeBatchInsert(List<?> list, Class<?> mapperType, String tableName) {
SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
List<Future<Void>> futures = new ArrayList<>();
Future<Void> future = executorService.submit(() -> {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
Object mapper = sqlSession.getMapper(mapperType);
Method method = findBatchInsertMethod(mapperType);
if (method != null) {
performBatchInsert(sqlSession, mapper, method, list, tableName);
}
} catch (Exception e) {
logger.error("异步批量插入失败", e);
}
return null;
});
futures.add(future);
return futures;
}
/**
* 判断异步任务是否执行完毕
* @param futures 异步任务列表
*/
public static void waitForAll(List<Future<Void>> futures) {
for (Future<Void> future : futures) {
try {
future.get();
} catch (ExecutionException | InterruptedException e) {
logger.error("等待任务失败", e);
}
}
}
}
@BatchInsert
注解在需要进行批量插入的方法上添加@BatchInsert
注解。例如:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
public interface MyMapper {
@BatchInsert
@Insert("INSERT INTO my_table (column1, column2) VALUES (#{item.column1}, #{item.column2})")
int insertItem(@Param("item") MyItem item);
@BatchInsert
@Insert("INSERT INTO ${tableName} (column1, column2) VALUES (#{item.column1}, #{item.column2})")
int insertItemByTableName(@Param("item") MyItem item, @Param("tableName") String tableName);
}
可以选择同步或异步批量插入方法,具体取决于需求。
假设我们有一个MyItem
类和一个MyMapper
接口。我们将演示如何使用BatchInsertUtil
进行批量插入操作。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
public class BatchInsertDemo {
public static void main(String[] args) {
// 创建待插入的对象列表
List<MyItem> itemList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
MyItem item = new MyItem();
item.setColumn1("Value1_" + i);
item.setColumn2("Value2_" + i);
itemList.add(item);
}
// 使用批量插入工具类进行批量插入
BatchInsertUtil.batchInsert(itemList, MyMapper.class);
// 使用多线程同步批量插入
BatchInsertUtil.syncBatchInsert(itemList, MyMapper.class);
// 使用多线程异步批量插入
List<Future<Void>> futures = BatchInsertUtil.asyncBatchInsert(itemList, MyMapper.class);
BatchInsertUtil.waitForAll(futures);
// 分表情况下的批量插入
String tableName = "my_table_partition";
BatchInsertUtil.batchInsert(itemList, MyMapper.class, tableName);
BatchInsertUtil.syncBatchInsert(itemList, MyMapper.class, tableName);
futures = BatchInsertUtil.asyncBatchInsert(itemList, MyMapper.class, tableName);
BatchInsertUtil.waitForAll(futures);
}
}
// MyItem类的定义
class MyItem {
private String column1;
private String column2;
// Getters and Setters
public String getColumn1() {
return column1;
}
public void setColumn1(String column1) {
this.column1 = column1;
}
public String getColumn2() {
return column2;
}
public void setColumn2(String column2) {
this.column2 = column2;
}
}
创建待插入的对象列表:创建一个包含需要插入数据的列表itemList
。
调用批量插入方法
:
BatchInsertUtil.batchInsert
方法进行简单批量插入。BatchInsertUtil.syncBatchInsert
方法进行多线程同步批量插入。BatchInsertUtil.asyncBatchInsert
方法进行多线程异步批量插入,并使用BatchInsertUtil.waitForAll
方法等待所有异步任务完成。分表情况下的批量插入:在需要分表插入的情况下,提供表名参数。
SpringContextUtil
已正确配置并能够获取到SqlSessionFactory
。@BatchInsert
注解。futures
中的所有任务都已完成。通过以上步骤和示例代码,可以有效地使用BatchInsertUtil
进行批量插入操作,提高数据库操作的性能和效率。
MyBatis的批量插入使用这个工具类效率可以极大提升,当然基于这个思路还有更多优化的地方,本文仅抛砖引玉,希望大家提出更好的方案,让我们一起进步!
感谢观看 (* ̄︶ ̄)