大批量的不同类型的数据插入不同的表

最近有这么一个需求,前端页面选择Excel文件,将文件中的数据存到数据库中,然而这些Excel的模板有30+种,也就是每个Excel模板对应一张表,最简单的解决办法就是根据Excel模板类型写30+个if{}else{},基本步骤就是,判断类型,选择实体类,从Excel中取出数据,往实体中注入数据,调用插入方法。然后后边是无穷无尽的else.然而对于拒绝搬砖的我,当然拒绝了这个办法,也是为了方便以后功能的拓展,决定另辟蹊径,下面是我的心路历程。

一 反射

为了避免大量的if else ,当然首选设计模式中的策略模式,利用反射,灵活的选择需要注入数据的实体类和调用插入方法。而不是在代码中硬编码。于是我找到了jdk自带的通过反射注入属性值–PropertyDescriptor (具体内容可参考:https://blog.csdn.net/weixin_42069143/article/details/82119724)

package com.peidasoft.instrospector;  

import java.beans.BeanInfo;  
import java.beans.Introspector;  
import java.beans.PropertyDescriptor;  
import java.lang.reflect.Method;  

public class BeanInfoUtil {  

    // 设置bean的某个属性值  
    public static void setProperty(UserInfo userInfo, String userName) throws Exception {  
        // 获取bean的某个属性的描述符  
        PropertyDescriptor propDesc = new PropertyDescriptor(userName, UserInfo.class);  
        // 获得用于写入属性值的方法  
        Method methodSetUserName = propDesc.getWriteMethod();  
        // 写入属性值  
        methodSetUserName.invoke(userInfo, "wong");  
        System.out.println("set userName:" + userInfo.getUserName());  
    }  

    // 获取bean的某个属性值  
    public static void getProperty(UserInfo userInfo, String userName) throws Exception {  
        // 获取Bean的某个属性的描述符  
        PropertyDescriptor proDescriptor = new PropertyDescriptor(userName, UserInfo.class);  
        // 获得用于读取属性值的方法  
        Method methodGetUserName = proDescriptor.getReadMethod();  
        // 读取属性值  
        Object objUserName = methodGetUserName.invoke(userInfo);  
        System.out.println("get userName:" + objUserName.toString());  
    }  
}

然而我在测试的过程中,遇到了一个很麻烦的问题,Excel中的数据取出时是按照String类型存储在map中的,然后对于数字类型和datetime类型,实体类中是Integer和dateTime类型,这个不能很好的进行类型转换(所以只在测试阶段用了一下,没能实际运用到项目中。)查了一下,可以转换,需要写工具类,我就是闲麻烦,又在找资料,于是我又找到了Apache的common-beanutils。

二、common-beanutils 基于反射实现灵活的封装数据

首先这是maven链接:https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils
这个使用起来就非常方便。下面是测试内容,一些疑问和尝试都在注释中了。

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import java.lang.reflect.InvocationTargetException;

public class BeanUtilsCommonReject {
    public static void main(String[] args) {
        User userInfo = new User();
        Dept dept = new Dept();
        dept.setDeptId(1L);
        try {
       		 /**
             * BeanUtils.setProperty()属性分别为实体类,属性名和属性值
             */
            BeanUtils.setProperty(userInfo, "userName", "peida");
            System.out.println("set userName:" + userInfo.getUserName());
            System.out.println("get userName:" + BeanUtils.getProperty(userInfo, "userName"));
            /**
             * 如果是参数的值得类型无法转换为目标属性的类型,则取值为目标属性的默认值
             * 比如此处userID为long型,参数为“aaaa",字母无法转换为long,则此处userID取默认值为0
             */
            BeanUtils.setProperty(userInfo, "userId", "18");
            System.out.println("set userId:" + userInfo.getUserId());
            System.out.println("get userId:" + BeanUtils.getProperty(userInfo, "userId"));
            System.out.println("get userName type:" + 
            	BeanUtils.getProperty(userInfo, "userName").getClass().getName());
            System.out.println("get userId type:" + 
            	BeanUtils.getProperty(userInfo, "userId").getClass().getName());

            /**
             * 可以支持直接赋值对象,也可以支持嵌套赋值直接赋值内置对象的属性值
             */
            BeanUtils.setProperty(userInfo,"createTime", DateUtils.getNowDate());
            System.out.println("get createTime:"+BeanUtils.getProperty(userInfo,"createTime"));
            //赋值对象
            BeanUtils.setProperty(userInfo,"dept",dept);
            //赋值对象的属性值
            BeanUtils.setProperty(userInfo,"dept.deptId","1");
            System.out.println("get dept:"+dept);
			
			/**
             * PropertyUtils类和BeanUtils不同在于,运行getProperty、setProperty操作时,没有类型转换,
             * 使用属性的原有类型或者包装类。
             * 由于userId属性的数据类型是long,所以方法
             * PropertyUtils.setProperty(userInfo,”userId”, “8”)会爆出数据类型不匹配,
             * 无法将值赋给属性。
             */
            PropertyUtils.setProperty(userInfo, "userId", 8);
            System.out.println(PropertyUtils.getProperty(userInfo, "userId"));
            System.out.println(PropertyUtils.getProperty(userInfo, "userId").getClass().getName());
            PropertyUtils.setProperty(userInfo, "userId", 8);  // IllegalArgumentException
        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

应用到实际中是这样的

    //读取第一页
    ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
    List<Map<String,Object>> readAll = reader.readAll();
    List<List<Object>> readAll1 = reader.read();
    Object map11 = readAll1.get(0);
    UploadTemplete uploadTemplete = 
    	uploadTempleteService.selectUploadTempleteByCode(recordEntity.getTemplateCode());
    //去掉空格
    uploadTemplete.setExcelHeaderName(uploadTemplete.getExcelHeaderName().replace(" ",""));
    uploadTemplete.setTableColumnName(uploadTemplete.getTableColumnName().replace(" ",""));

    //验证模板是否一致
    if (!uploadTemplete.getExcelHeaderName().equals(
    	StringUtils.strip(map11.toString(),"[]").replace(" ",""))){
    	
        result.put("state", "error");
        result.put("msg", "上传的excel文件与模板不一致,请检查");
        return result;
    }
    if(readAll.size()<=0) {
        result.put("state", "error");
        result.put("msg", "上传的excel文件数据不能为空");
        return result;
    }
    //包名首字母小写
    String packageName = StringUtils.toCamelCase(uploadTemplete.getTableName());
    //实体类首字母大写
    String domainName = StringUtils.convertToCamelCase(uploadTemplete.getTableName());

    //实体类,拼接实体类的路径和类名,利用反射加载实体类的class对象
    Class<?> clazzDomain = Class.forName("com.aaa.aaa." + packageName + ".domain." + domainName);
    //实例化实体对象
    Object objectDomain = clazzDomain.newInstance();

    //service类,拼接实体类的路径和类名
    Class<?> clazzService = Class.forName(
    	"com.aaa.aaa." + packageName + ".service.I" + domainName + "Service");
    //由于service已经交给spring控制了,所以service实例从spring容器中取出
    Object objectService = SpringUtils.getBean(clazzService);
    //service类中插入方法
    Method serviceMethod = clazzService.getMethod("insert"+domainName, clazzDomain);
    //因为不能采用硬编码方式,所以Excel列名不能存在在代码中,所以将列放在了数据库中,用逗号隔开,
    //取出时通过转换成数组
    String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
    //这里边存的是与Excel模板相对应的数据库中表的列的名字
    String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
    for(int i=0;i<readAll.size();i++) {
        Map<String, Object> readMap = readAll.get(i);
        //给实体属性注入值
        BeanUtils.setProperty(objectDomain,"code",UUIDUtil.getUUID32());
        //循环取出Excel中每一行的数据,
        for (int j=0;j<columnArr.length;j++){			
        	BeanUtils.setProperty(objectDomain,StringUtils.toCamelCase(columnArr[j]),
        		readMap.get(excelHeaderArr[j]).toString());
        }
        BeanUtils.setProperty(objectDomain,"del_flag","0");
        BeanUtils.setProperty(objectDomain,"createCode",recordEntity.getCreateCode());
        BeanUtils.setProperty(objectDomain,"createTime",DateUtils.getNowDate());
		//执行插入方法
        serviceMethod.invoke(objectService,objectDomain);
    }

然而这个方法还是有很致命的硬伤,数据单条插入,数据量太大的话,会花费很长时间,给用户不好的体验。所以首先想到的就是批量插入,但是现在每个表的mybatis xml文件还没有批量插入的方法,如果写批量插入的话,要写30+个,所以就先放弃了。采用了另一种方法,利用map代替实体。

三、map代替实体类

利用实体类进行封装数据没问题,其实已经很好了,但是批量插入的话,要写30+条批量插入的语句,而且每个还都不一样,所以想到了用map做到进一步解耦。
下面代码就简单多了,就是将实体类换成了map,代码如下:

String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
//因为没有实体关联,不能调用具体service中的插入方法,所以这里需要注入tableName控制插入的那个表中
String tableName = uploadTemplete.getTableName();
for(int i=0;i<readAll.size();i++) {
    Map<String, Object> readMap = readAll.get(i);
    Map<String, Object> map = new HashMap<>();
    //循环取出Excel中每一行的数据
    for (int j=0;j<columnArr.length;j++){
        map.put(columnArr[j],readMap.get(excelHeaderArr[j]));
    }
    map.put("del_flag","0");
    map.put("create_code",recordEntity.getCreateCode());
    map.put("create_time",DateUtils.getNowDate());
    uploadTempleteService.insertRecordsByTableName(map,tableName);

当然了,对于这一块,只有逻辑代码是不够的,下面请看一下dao层的代码和sql
首先是dao接口的方法

public int insertRecordsByTableName(@Param("map") Map<String,Object> map,
	@Param("tableName) tableName);

下面是sql ,在这我又遇到了难点,怎么循环取出map中所有的key和value,幸好mybatis 中foreach标签提供了循环map的功能,

insert into ${tableName}
//在foreach循环map的时候,collection使用map.entrySet()取出map中所有的键值对,然后index存的是key,
//item存的是value
<foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
 	${key}
 </foreach>
 values
 <foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
     <if test="key != 'tableName'">
         #{value}
     </if>
 </foreach>

这样就实现了功能与实体主键的解耦。
然后,,,这样就结束了吗,还没有我们上边说的批量插入还没有实现。下边就是批量插入了。

四、List<Map<String,Object>实现批量插入

首先是逻辑代码也是很简单,就是将第三部的map放到list中实现批量插入。

List<Map<String,Object>> list = new ArrayList<>();
String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
for(int i=0;i<readAll.size();i++) {
    Map<String, Object> readMap = readAll.get(i);
    Map<String, Object> map = new HashMap<>();
    for (int j=0;j<columnArr.length;j++){
        if(StringUtils.isEmpty(readMap.get(excelHeaderArr[j]).toString())){
            return returnMsg(excelHeaderArr[j],0,(i+2));
        }
        map.put(columnArr[j],readMap.get(excelHeaderArr[j]));
    }
    map.put("del_flag","0");
    map.put("create_code",recordEntity.getCreateCode());
    map.put("create_time",DateUtils.getNowDate());
    list.add(map);
}
//批量插入
uploadTempleteService.batchInsertRecordsByTableName(list,uploadTemplete.getTableName());

逻辑代码依然是如此简单,重点还是在sql。在写这个sql的时候又遇到了一个难题,怎么在mybatis中实现双重循环,因为上边的map需要循环取出列的数据,现在又套了一层list,所以需要双层循环。请看代码

public int batchInsertRecordsByTableName(@Param("list") List<Map<String,Object>> list,
	@Param("tableName") String tableName);
insert into ${tableName}(
<foreach collection="list" separator="," index="index" item="map">
	//因为sql中需要插入的列名只需要取出一次就可以了,所以使用list的index控制循环次数。
    <if test="index == 1">
        <trim suffixOverrides=",">
            <foreach item="value" index="key" collection="map.entrySet()">
                ${key},
            </foreach>
        </trim>
    </if>
</foreach>
)values
<foreach collection="list" separator="," index="index" item="map">
    <foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
        #{value}
    </foreach>
</foreach>

五、总结

这就是我对于开头提出的问题的一个解决历程。如此高度的封装虽然简化了代码,使代码看起来没有臃肿,很简洁,但是也会有一种问题,这么多模板不可能每一个都没有特殊的情况,比如控制一些必填项啊,数据累类型检验啊,对于添加这些功能来说有些许困难。

好了,今天分享就到这了。

参考:https://blog.csdn.net/weixin_42069143/article/details/82119724

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值