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

背景

最近项目快上线遇到数据迁移的问题,需要将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 page = shopService.searchPage();
         List 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 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 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> modelMap = new HashMap<>();
    
      @SafeVarargs
      TemplateEnum(Class columnClass,
                   Class... modelClass) {
          this(null, columnClass, modelClass);
      }
    
      @SafeVarargs
      TemplateEnum(TemplateEnum subTemplate, Class columnClass,
                   Class... modelClass) {
          this.subTemplate = subTemplate;
          this.columns = columnClass.getEnumConstants();
    
          for (Class aClass : modelClass) {
              String key = StringUtil.firstCharToLowerCase(aClass.getSimpleName());
              modelMap.put(key, aClass);
          }
      }
    
      public TemplateEnum getSubTemplate() {
          return subTemplate;
      }
    
      public Detail[] getColumns() {
          return columns;
      }
    
      public Map> 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 getConvertMap() {
              BiMap 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 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
      * 

    * 注意:此方法有SQL注入的可能,传入的参数不能直接来源用户请求 * * @param table 查询的表 * @param field 查询的字段名称 * @param value 查询的条件值 * @return 记录ID */ public List 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 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的组件设计)