概述
项目使用的架构是SpringBoot和Mybatis plus,在上传Excel的时候,通常会调用Mybatis plus的saveBatch往数据库中写入数据,我们知道Mybatis plus仅支持Mybatis plus实体进行批量写入,但是大多情况下,Mybatis plus实体Entity 和 Excel Entity是不同,这时候一般会选择用Apache的BeanUtils.copyProperties将Excel Entity转为Mybatis plus实体Entity,在调用Mybatis plus的saveBatch往数据库中写数据,如下
List<Object> list = EasyExcelUtil.EasyExcelReadUtil(fileName, StudentExcel.class);
List<Student> studentList = new ArrayList<>();
for(Object o : list){
StudentExcel studentExcel=(StudentExcel)o;
Student student = new Student();
BeanUtils.copyProperties(studentExcel,student);
}
IStudentService.saveBatch(studentList);
由于Apache的Beanutils.copyproperties 进行数据拷贝时,会进行重复的对象类型检查、转换,所以在数据量较大的情况下,性能表现并不是很好,
这里的话有两种办法能够使程序在较大数据量的情况下也能表现良好,
一种Spring BeanUtils 或者 Cglib BeanCopier 来代替Apache的 BeanUtils,
另一种是,可以仿着mybatis plus的saveBatch写一个方法,可以使不同的Excel Entity也能完成数据的批量写入,
本文用的是第二方法
案例
通过Mybatis Batch批量导入数据,避免了一条一条往数据库中插
@PostMapping("/insertStudet")
public Result uploadStudentInfo(@RequestParam MultipartFile fileName) throws Exception {
List<Object> list = EasyExcelUtil.EasyExcelReadUtil(fileName, StudentExcel.class);
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int size = 10000;
try{
for(int i = 0; i < list.size(); i++) {
StudentExcel studentExcel = (StudentExcel)list.get(i);
//批量设置id
studentExcel.setSid(UUID.randomUUID().toString().replace("-",""));
mapper.insertStudentExcel(studentExcel);
if(i % 1000 == 0 || i == size - 1) {
//手动每1000个一提交,提交后无法回滚
sqlSession.commit();
//清理缓存,防止溢出
sqlSession.clearCache();
}
}
sqlSession.commit();
} catch (Exception e) {
//没有提交的数据可以回滚
sqlSession.rollback();
} finally{
sqlSession.close();
}
return new Result(list);
}
Excel用的是阿里开源的EasyExcel进行解析
public class EasyExcelUtil {
//对EasyExcel的再封装
public static List<Object> EasyExcelReadUtil(MultipartFile multipartFile,Class clazz) throws Exception {
if (!multipartFile.getOriginalFilename().toLowerCase().endsWith(".xls")&&!multipartFile.getOriginalFilename().toLowerCase().endsWith(".xlsx")){
throw new Exception("文件格式错误!");
}
ExcelListener listener = new ExcelListener();
EasyExcel.read(multipartFile.getInputStream(), clazz, listener).sheet().doRead();
return listener.getList();
}
}
解析Excel必须创建的监听器
@Data
@Component
public class ExcelListener extends AnalysisEventListener<Object> {
private static final Logger log = LoggerFactory.getLogger(ExcelListener.class);
//当controller调用sheet().doRead(),就会调用监听器,就会将数据存储在list中了
List<Object> list = new ArrayList<>();
@Override
public void invoke(Object o, AnalysisContext analysisContext) {
list.add(o);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
实体类
@Data
public class StudentExcel {
private String sid;
@ExcelProperty(index = 0)
private String sname;
@ExcelProperty(index = 1)
private String sgrade;
@ExcelProperty(index = 2)
private String sclass;
@DateTimeFormat
@ExcelProperty(index = 3)
private Date countTime;
}
使用阿里的EasyExcel对Excel进行解析的时候,当sname写成sName,会导致这一列无法注入直接为空,而将其写成steudentName的时候又没有问题,我想应该是首字母后不能直接大写的原因吧,具体并未深究,应该注意一点就行
可以看到,批量插入成功
优化
大体思路是写一个泛型的BaseDao,需要批量插入的Dao继承这个BaseDao并传给我相应的Entity类型,再由调用者传给我,写好的Insert,这样就可以实现批量导入了
BaseDao:
public class BaseDao<T> {
@Resource
private SqlSessionTemplate sqlSessionTemplate;
protected Class<T> clazz;
protected SqlSession getSession(){
//开启批量插入操作
return sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
}
public BaseDao(){
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();// 获得直接超类
clazz = (Class) type.getActualTypeArguments()[0];
}
public int save(T entity,String sqlStatement ){
SqlSession session = getSession();
int insertCount = session.insert(sqlStatement, entity);
session.flushStatements();
return insertCount;
}
public boolean saveExcel(List<Object> list,String sqlStatement){
SqlSession session = getSession();
int i=0;
for (Object oneEntity : list){
session.insert(sqlStatement,oneEntity);
if (i >= 1 && i % 1000 == 0){
session.flushStatements();
}
i++;
}
session.flushStatements();
return true;
}
//保存记录
public boolean saveBatch(Collection<T> entityList,String sqlStatement){
SqlSession session = getSession();
int i=0;
for (T oneEntity : entityList){
session.insert(sqlStatement,oneEntity);
if (i >= 1 && i % 1000 == 0){
session.flushStatements();
}
i++;
}
session.flushStatements();
return true;
}
}
StudentExcelDao:
@Repository
public class StudentExcelDao extends BaseDao<StudentExcel>{
}
Mapper:
<insert id="insertStudentExcel" parameterType="com.bill.entity.StudentExcel">
INSERT INTO STUDENT(SID,SNAME,SGRADE,SCLASS,COUNT_TIME) VALUES (#{sid},#{sname},#{sgrade},#{sclass},#{countTime})
</insert>
说优化吧,也不是很优化,能够做到的是相对通用一点,但是它仍然要每次都在Mapper中创建insertExcel结果看,而且Id也暂时不支持自动填充,这样的话是比不上使用Spring BeanUtils类的,先暂且做个探究,以后再完善
最后:
可以认识到的一点是,重写Mybits的底层的话,需要得到,需要得到Mapper中路径,然后还要得到实体的与表的映射信息,然后将Mapper路径和实体映射信息都拼在SQL中
然后使用sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false),开启Mybatis的批量写入方法,再用SqlSession的insert方法就可以当数据量达到指定数量时一次性写入到数据库中,这样就能减少数据库写入数据的压力,并完成数据的一次性写入,这个实现可能还需要一段时间,可能反射的类层次设计的知识并不够,但思路应该是这个思路
补充一点:
看这样一段代码,是什么意思呐?它的意思是指,当调用子类时,获得子类继承父类的第一个泛型类,比如当你通过StudentExcelDao调用BaseDao的save方法时,获得的就是StudentExcel类