背景
最近项目快上线遇到数据迁移的问题,需要将excel中的数据导入到DB中,并支持部分重要数据导出备份。
问题
- 导入与导出的excel种类多
- 基于poi实现的重复代码多
思路
- 将excel的导入与导出逻辑基于poi进一步封装。封装点如下:
- 导出的数据直接来源SQL查询结果(无需单独填充单元格)
- 通过对象属性与列的绑定实现单元格数据的自动填充与反向解析
- 导入数据直接被解析为DB的实体Bean对象组集合(无需单独填充Bean属性)
设计
- 难点
- excel一行的数据可来源多张表,且存在A表一条数据对应B表多条数据
- 对象属性与列的双向绑定,怎么获取值填充对应列
- 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); } } -
通过枚举定义简单模型
- 利用嵌套模型实现A类表对应多条B表的情况
- 利用模型属性实现通用逻辑的处理,如隐藏列、非空
- 利用BiMap实现简单的excel展示值与DB存储值的双向转换
- 利用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
非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
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(); } } }