美文网首页
Java导入与导出Excel的组件设计

Java导入与导出Excel的组件设计

作者: XJ2017 | 来源:发表于2020-03-15 16:00 被阅读0次

    背景

    最近项目快上线遇到数据迁移的问题,需要将excel中的数据导入到DB中,并支持部分重要数据导出备份。

    问题

    • 导入与导出的excel种类多
    • 基于poi实现的重复代码多

    思路

    • 将excel的导入与导出逻辑基于poi进一步封装。封装点如下:
      1. 导出的数据直接来源SQL查询结果(无需单独填充单元格)
      2. 通过对象属性与列的绑定实现单元格数据的自动填充与反向解析
      3. 导入数据直接被解析为DB的实体Bean对象组集合(无需单独填充Bean属性)

    设计

    • 难点
      1. excel一行的数据可来源多张表,且存在A表一条数据对应B表多条数据
      2. 对象属性与列的双向绑定,怎么获取值填充对应列
      3. excel展示值与DB存储值的双向转换
    • 方案
      • 难点一:在处理完A类表数据后,通过回调函数获取对应的B表多条记录。A类表数据合并单元格,B表每条记录占一行
      • 难点二:每个excel建立一个简单模型,使用ognl表达式标识对象属性,使用名称标识列。通过ognl表达式获取属性值
      • 难点三:在模型中建立转换函数

    Java代码实现

    • 导入与导出控制层处理(ShopSellRentInfo为DB表对象组、Shop为DB表对象)

      @PostMapping("/export/shop")
       public void exportShop(HttpServletResponse response) {
           // 查询需要导出的数据
           Page<ShopComplex.ShopSellRentInfo> page = shopService.searchPage();
           List<ShopComplex.ShopSellRentInfo> infoList = page.getResults();
      
           // 渲染导出数据并导出
           ExcelUtil.export(TemplateEnum.SHOP, infoList, "档位信息", response);
       }
      
       @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = RuntimeException.class)
       @PostMapping("/import/shop")
       public void importShop(MultipartFile file) {
           List<ImportRow> rowList = ExcelUtil.importExcel(TemplateEnum.SHOP, file, excelDao);
      
           // 将Bean数据同步到DB中
           for (ImportRow importRow : rowList) {
               Shop shop = importRow.getBean(Shop.class);
               shopService.addOrUpdateExcel(shop);
           }
       }
      
    • 通过枚举定义简单模型

      1. 利用嵌套模型实现A类表对应多条B表的情况
      2. 利用模型属性实现通用逻辑的处理,如隐藏列、非空
      3. 利用BiMap实现简单的excel展示值与DB存储值的双向转换
      4. 利用ExcelDao实现复杂的excel展示值与DB存储值的单向转换(通过某个字段值获取ID)
       /**
       * 导入与导出的Excel模板枚举
       *
       * @author XiaoJia
       * @since 2020/3/11 21:49
       */
      public enum TemplateEnum {
      
        /**
         * 档位
         */
        SHOP(ShopEnum.class, Shop.class),
      
        /**
         * 档位租赁 的 子模板
         */
        SHOP_LEASE_CHARGE(ShopLeaseChargeEnum.class, ShopLeaseCharge.class),
        /**
         * 档位租赁
         */
        SHOP_LEASE_ORDER(TemplateEnum.SHOP_LEASE_CHARGE, ShopLeaseOrderEnum.class, ShopLeaseOrder.class);
      
      
        interface Detail {
            /**
             * 将数据库与excel展示的字符建立一一映射
             *
             * @return 选项值
             */
            default BiMap<Object, String> getConvertMap() {
                return null;
            }
      
            /**
             * 将OGNL解析的值进行再一步转换
             *
             * @param export   导出,true-导出、false-导入
             * @param origin   原始的值
             * @param excelDao 数据访问层对象
             * @return 转换后的值
             */
            default Object convert(boolean export, Object origin, ExcelDao excelDao) {
                if (getConvertMap() == null) {
                    return origin;
                }
      
                return (export ? getConvertMap() : getConvertMap().inverse()).get(origin);
            }
      
            /**
             * 获取sheet中的列名
             *
             * @return sheet中的列名
             */
            String getName();
      
            /**
             * 获取列值的OGNL表达式
             *
             * @return 列值的OGNL表达式
             */
            String getOgnl();
      
            /**
             * 获取是否隐藏列
             *
             * @return 是否隐藏列
             */
            boolean isHidden();
      
            /**
             * 获取是否导入列
             *
             * @return 是否导入列
             */
            boolean isImportable();
      
            /**
             * 获取是否可为空
             *
             * @return 是否可为空
             */
            boolean isNullable();
        }
      
        /**
         * 子模板
         */
        private TemplateEnum subTemplate;
        /**
         * 属于该模板的所有列信息
         */
        private Detail[] columns;
        /**
         * 保持模板导入时新增或更新的实体类
         */
        private Map<String, Class<? extends BaseModel>> modelMap = new HashMap<>();
      
        @SafeVarargs
        TemplateEnum(Class<? extends Detail> columnClass,
                     Class<? extends BaseModel>... modelClass) {
            this(null, columnClass, modelClass);
        }
      
        @SafeVarargs
        TemplateEnum(TemplateEnum subTemplate, Class<? extends Detail> columnClass,
                     Class<? extends BaseModel>... modelClass) {
            this.subTemplate = subTemplate;
            this.columns = columnClass.getEnumConstants();
      
            for (Class<? extends BaseModel> aClass : modelClass) {
                String key = StringUtil.firstCharToLowerCase(aClass.getSimpleName());
                modelMap.put(key, aClass);
            }
        }
      
        public TemplateEnum getSubTemplate() {
            return subTemplate;
        }
      
        public Detail[] getColumns() {
            return columns;
        }
      
        public Map<String, Class<? extends BaseModel>> getModelMap() {
            return modelMap;
        }
      }
      

      非A类表对应B表的案例

      /**
       * @author XiaoJia
       * @since 2020/3/11 23:34
       */
      public enum ShopEnum implements TemplateEnum.Detail {
      
        /**
         *
         */
        AREA_NAME("区域", "area.name", false, false, true),
        HOUSE_NAME("栋号", "house.name", false, true, false),
        ID("档位ID", "shop.id", true, true, true),
        USE_STATUS("使用情况", "shop.useStatus", false, true, false) {
            @Override
            public BiMap<Object, String> getConvertMap() {
                BiMap<Object, String> biMap = HashBiMap.create();
                biMap.put(0, "空闲");
                biMap.put(1, "正常经营");
                return biMap;
            }
        },
      
        ;
      
        private String name;
        private String ognl;
        private boolean hidden;
        private boolean importable;
        private boolean nullable;
      
        ShopEnum(String name, String ognl, boolean hidden, boolean importable, boolean nullable) {
            this.name = name;
            this.ognl = ognl;
            this.hidden = hidden;
            this.importable = importable;
            this.nullable = nullable;
        }
      
        @Override
        public String getName() {
            return name;
        }
      
        @Override
        public String getOgnl() {
            return ognl;
        }
      
        @Override
        public boolean isHidden() {
            return hidden;
        }
      
        @Override
        public boolean isImportable() {
            return importable;
        }
      
        @Override
        public boolean isNullable() {
            return nullable;
        }
      }
      

      A类表对应B表的案例

      public enum ShopLeaseChargeEnum implements TemplateEnum.Detail {
        /**
         *
         */
        ID("档位租赁费用ID", "shopLeaseCharge.id", true, true, true),
        PRICE("价格", "shopLeaseCharge.price", false, true, false),
        ;
      
        private String name;
        private String ognl;
        private boolean hidden;
        private boolean importable;
        private boolean nullable;
      
        ShopLeaseChargeEnum(String name, String ognl, boolean hidden, boolean importable, boolean nullable) {
            this.name = name;
            this.ognl = ognl;
            this.hidden = hidden;
            this.importable = importable;
            this.nullable = nullable;
        }
      
        @Override
        public String getName() {
            return name;
        }
      
        @Override
        public String getOgnl() {
            return ognl;
        }
      
        @Override
        public boolean isHidden() {
            return hidden;
        }
      
        @Override
        public boolean isImportable() {
            return importable;
        }
      
        @Override
        public boolean isNullable() {
            return nullable;
        }
      
      }
      
      public enum ShopLeaseOrderEnum implements TemplateEnum.Detail {
      
        /**
         *
         */
        ID("档位租赁ID", "shopLeaseOrder.id", true, true, true),
        CUSTOMER_NAME("商户", "customer.name", false, true, false),
        AREA_NAME("区域", "area.name", false, false, true),
        HOUSE_NAME("栋号", "house.name", false, false, true),
        SHOP_NUM("档位号", "shop.number", false, true, false),
        GOOD_TYPE_NAME("主营品种", "goodType.name", false, true, false),
        WATER_BEGIN("水表基数", "shopLeaseOrder.waterBegin", false, true, false),
        WATER_RELATED_SHOP_ID("关联水表档位", "shopLeaseOrder.waterRelatedShopId", false, true, true) {
            @Override
            public Object convert(boolean export, Object origin, ExcelDao excelDao) {
                return convertShop(export, origin, excelDao);
            }
        },
        WATER_RELATED_BEGIN("关联水表度数", "shopLeaseOrder.waterRelatedBegin", false, true, true),
        ;
      
        private String name;
        private String ognl;
        private boolean hidden;
        private boolean importable;
        private boolean nullable;
      
        ShopLeaseOrderEnum(String name, String ognl, boolean hidden, boolean importable, boolean nullable) {
            this.name = name;
            this.ognl = ognl;
            this.hidden = hidden;
            this.importable = importable;
            this.nullable = nullable;
        }
      
        @Override
        public String getName() {
            return name;
        }
      
        @Override
        public String getOgnl() {
            return ognl;
        }
      
        @Override
        public boolean isHidden() {
            return hidden;
        }
      
        @Override
        public boolean isImportable() {
            return importable;
        }
      
        @Override
        public boolean isNullable() {
            return nullable;
        }
      
        private static Object convertShop(boolean export, Object origin, ExcelDao excelDao) {
            if (ObjectUtil.isEmpty(origin)) {
                return origin;
            }
      
            if (export) {
                return excelDao.searchById(Shop.class, (String) origin).getNumber();
            } else {
                List<String> idList = excelDao.searchId("shop", "number", (String) origin);
                if (ObjectUtil.isEmpty(idList) || idList.size() >= 2) {
                    throw new BusinessException("%s 不唯一或不存在!", origin);
                }
                return idList.get(0);
            }
        }
      
      }
      
    • 由于具体实现与现有公司平台有一定耦合,所以不贴具体实现代码。以下是直接访问DB的案例

      /**
      * @author XiaoJia
      * @since 2020/3/12 21:12
      */
      @Component
      public class ExcelDao {
      
       @Autowired
       private SqlSessionFactory sqlSessionFactory;
      
       @Autowired
       @Qualifier("sqlSessionTemplate")
       private SqlSession sqlSession;
      
       @PostConstruct
       public void init() {
           sqlSessionFactory.getConfiguration().addMapper(ExcelMapper.class);
       }
      
       /**
        * 通过某个字段值查询记录ID
        * <p>
        * 注意:此方法有SQL注入的可能,传入的参数不能直接来源用户请求
        *
        * @param table 查询的表
        * @param field 查询的字段名称
        * @param value 查询的条件值
        * @return 记录ID
        */
       public List<String> searchId(String table, String field, String value) {
           ExcelMapper mapper = sqlSession.getMapper(ExcelMapper.class);
           return mapper.searchId(table, field, value);
       }
      
       public interface ExcelMapper {
           /**
            * 通过某个字段值查询记录ID
            *
            * @param table 查询的表
            * @param field 查询的字段名称
            * @param value 查询的条件值
            * @return 记录ID集合
            */
           @SelectProvider(type = ExcelProvider.class, method = "searchId")
           List<String> searchId(@Param("table") String table, @Param("field") String field, @Param("value") String value);
      
       }
      
       public static class ExcelProvider {
           /**
            * 通过某个字段值查询记录ID
            *
            * @param table 查询的表
            * @param field 查询的字段名称
            * @param value 查询的条件值
            * @return 查询SQL
            */
           public String searchId(@Param("table") String table, @Param("field") String field,
                                  @Param("value") String value) {
               return new SQL() {
                   {
                       SELECT("id");
                       FROM(table);
                       WHERE(field + " = '" + value + "'");
                   }
               }.toString();
           }
      
       }
      
      }
      

    相关文章

      网友评论

          本文标题:Java导入与导出Excel的组件设计

          本文链接:https://www.haomeiwen.com/subject/kaqrehtx.html