Spring boot读取Excel并存入PG数据库(一)
目录
一、前言
二、项目需求
三、数据库表设计
四、代码实现和测试
五、总结
一、前言
进入9月以来,一直忙于项目,特别是临近国庆节这半个月来,被临时拉入新的项目组,需求变化莫测,项目时间很是紧迫。我最开始并非专职Java开发,本职是大数据开发,奈何公司环境如此,也时常参与Java项目开发。很久没有认真写博客了,经历了整个忙碌的夏天,特别是9月以来,笔者有感于所经手的项目收获不小,有很多值得总结和反复推敲之处,故写下这几篇系列文章,在回顾和总结时,也希望本系列文章能够对大家有所帮助。如有不当之处,敬请指正!
本文为系列文章第一篇。
二、项目需求
废话不多说,先上需求:
拿到需求清单后,仔细查看,结合团队项目会议内容,总结出第一步也是最基础最核心的一步就是将客户的Excel表格数据导入后台PG数据库指定表。结合我们的项目架构采用的是spring boot+Mybatis,其实用自己的话来说就是实现Spring boot读取Excel并存入PG数据库,此为后端需要实现的功能,需要提供给前端一个读取Excel表格的接口,并实现将数据导入数据库。
明确了后端需要干什么,那接下来就比较清晰了。
三、数据库表设计
首选在测试库中设计所需的测试表,包含表名,表字段,关联字段等等。经过和其他开发协调沟通后,确定了测试表如下:
导入数据存储表SQL代码段:
-- ------------------------------ Table structure for zgq_user-- ----------------------------DROP TABLE IF EXISTS "public"."zgq_user";CREATE TABLE "public"."zgq_user" ("id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL DEFAULT NULL,"xm" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL,"xb" varchar(10) COLLATE "pg_catalog"."default" DEFAULT NULL,"sfzh" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL,"gx" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL,"dz" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL,"c_name" varchar(20) COLLATE "pg_catalog"."default" DEFAULT NULL,"ad_code" varchar(20) COLLATE "pg_catalog"."default" DEFAULT NULL,"ad_name" varchar(20) COLLATE "pg_catalog"."default" DEFAULT NULL,"update_by" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL,"update_time" date DEFAULT NULL,"bz" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL,"huhao" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL,"bgzt" varchar(10) COLLATE "pg_catalog"."default" DEFAULT NULL);COMMENT ON COLUMN "public"."zgq_user"."id" IS '主键';COMMENT ON COLUMN "public"."zgq_user"."xm" IS '姓名';COMMENT ON COLUMN "public"."zgq_user"."xb" IS '性别';COMMENT ON COLUMN "public"."zgq_user"."sfzh" IS '身份证号';COMMENT ON COLUMN "public"."zgq_user"."gx" IS '关系';COMMENT ON COLUMN "public"."zgq_user"."dz" IS '地址';COMMENT ON COLUMN "public"."zgq_user"."c_name" IS '所属村';COMMENT ON COLUMN "public"."zgq_user"."ad_code" IS '行政区划编码';COMMENT ON COLUMN "public"."zgq_user"."ad_name" IS '行政区划名称';COMMENT ON COLUMN "public"."zgq_user"."update_by" IS '操作人';COMMENT ON COLUMN "public"."zgq_user"."update_time" IS '操作时间';COMMENT ON COLUMN "public"."zgq_user"."bz" IS '备注';COMMENT ON COLUMN "public"."zgq_user"."huhao" IS '户号';COMMENT ON COLUMN "public"."zgq_user"."bgzt" IS '当前变更状态:默认未空';COMMENT ON TABLE "public"."zgq_user" IS '资格权-用户记录表';-- ------------------------------ Primary Key structure for table zgq_user-- ----------------------------ALTER TABLE "public"."zgq_user" ADD CONSTRAINT "zgq_user_pkey" PRIMARY KEY ("id");
四、代码实现和测试
实现spring boot读取Excel数据并存入PG数据库,将其拆解,第一是加载或者上传Excel表格;第二读取Excel数据;第三是将数据写入PG数据表中。
本次代码其实并非完全笔者亲自写的,而且借用了同事之前写好的基础上改进的。
1、长传Excel文件接口实现
controller层:
package com.fw.hs.controller;import com.fw.hs.vo.ExcelImportVo;import com.fw.hs.service.ExcelService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.AllArgsConstructor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;/*** @author * @date /9/8 10:09*/@Api(tags = "excel接口")@RestController@AllArgsConstructor@RequestMapping("/api/excel")public class ExcelController {@Autowiredprivate ExcelService excelService;@ApiOperation(value = "解析excel导入数据", notes = "其他备注")@PostMapping("/import")public boolean importData(@RequestParam(name="file",required = false) MultipartFile file, ExcelImportVo vo)throws IllegalAccessException, IOException, InstantiationException {excelService.importData(file, vo.getTableName(), vo.getColumns());return true;}}
service层:
package com.fw.hs.service;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.List;/*** @author * @date /9/8 10:26*/public interface ExcelService {void importData(MultipartFile file, String tableName, List<String> columns) throws IOException, InstantiationException, IllegalAccessException;}
2、读取Excel表格数据
Excel工具类:
package com.fw.hs.utils;import mons.lang3.StringUtils;import org.apache.poi.hssf.usermodel.HSSFWorkbook;import org.apache.poi.ss.usermodel.Cell;import org.apache.poi.ss.usermodel.CellType;import org.apache.poi.ss.usermodel.Row;import org.apache.poi.ss.usermodel.Sheet;import org.apache.poi.xssf.usermodel.XSSFWorkbook;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.ArrayList;import java.util.List;/*** @author * @date /9/8 10:47*/public class ExcelUtil {public static List<List<String>> readExcelFile(MultipartFile file, int length) throws IllegalAccessException, InstantiationException, IOException {List<List<String>> res = new ArrayList<>();String fileName = file.getOriginalFilename();Sheet sheet = null;if (StringUtils.isNotBlank(fileName)) {String suffix = fileName.substring(fileName.indexOf("."));if ("xls".equals(suffix)) {HSSFWorkbook wb = new HSSFWorkbook(file.getInputStream());sheet = wb.getSheetAt(0);} else {XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream());sheet = wb.getSheetAt(0);}}if (sheet == null) {return res;}for (int j = 1;j < sheet.getPhysicalNumberOfRows();j++){List<String> list = new ArrayList<>();Row row = sheet.getRow(j);if (null != row){for (int i = 0; i < length; i++) {Cell cell = row.getCell(i);if (cell != null) {row.getCell(i).setCellType(CellType.STRING);list.add(row.getCell(i).getStringCellValue().replaceAll(" ", ""));}}}res.add(list);}return res;}}
3、将数据写入PG数据库
service实现类:
package com.fw.hs.service.impl;import com.fw.hs.utils.ExcelUtil;import mon.base.support.IdGenerator;import com.fw.hs.mapper.ExcelMapper;import com.fw.hs.service.ExcelService;import lombok.extern.slf4j.Slf4j;import mons.collections4.CollectionUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.List;/*** @author * @date /9/8 10:26*/@Service@Slf4jpublic class ExcelServiceImpl implements ExcelService {@Autowiredprivate ExcelMapper excelMapper;@Override@Transactional(rollbackFor = Exception.class)public void importData(MultipartFile file, String tableName, List<String> columns) throws IOException, InstantiationException, IllegalAccessException {if (CollectionUtils.isEmpty(columns)) {// 获取数据库字段名columns = excelMapper.getTableColumns(tableName);}log.info("表:{},字段:{} ", tableName, columns);List<List<String>> lists = ExcelUtil.readExcelFile(file, columns.size());for (List<String> list: lists) {if (CollectionUtils.isNotEmpty(list)) {list.add(0, String.valueOf(IdGenerator.nextId()));log.info("表:{} 导入数据:{}", tableName, list);excelMapper.insert(tableName, columns, list);}}}}
Mybatis配置:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-////DTD Mapper 3.0//EN" "/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.fw.hs.mapper.ExcelMapper"><select id="getTableColumns" resultType="java.lang.String">SELECT A.attname AS NAMEFROMpg_class AS C,pg_attribute AS AWHEREC.relname = #{tableName}AND A.attrelid = C.oidAND A.attnum >0</select><insert id="insert">insert into ${tableName}<foreach item="column" index="index" collection="columns" open="(" separator="," close=")">${column}</foreach><foreach item="value" index="index" collection="values" open="values (" separator="," close=")">#{value}</foreach></insert></mapper>
4、测试
测试数据格式:
测试接口:
依次填入Excel对应的字段,一个不多,一个不少。点击选择读取文件,输入导入数据存储表zgq_user表名。然后运行后天进行测试,如图:
五、总结
由于项目紧急,需要在一周左右出一个版本,团队leader指定在原有接口基础上改,而接手时又没有任何资料或文档。虽然本次接口开发实现了基本的数据导入功能,但是考虑到具体业务和客户需求变更,总结出以下几点:
后续需求变更为所有功能从前端提供给客户,当客户点击读取Excel表格时,要让客户能明显感觉到导入数据的过程。而且客户那边要求仅仅他们所需的操作仅仅是选择所要读取的Excel文件,而本接口需要指定配置字段,存储表表名已经选择上传文件,该接口显然不符合最新的客户需求。测试数据为3000+时,导入数据时间约为4分30秒。考虑到客户数据量有几十万条(当然了,这是最开始反馈过来的信息,数据总量为近百万条级别),所以即使是试用版本也需要解决数据导入执行过程过长导致前端响应超时问题。在导入数据过程中实现输出数据导入日志,并按照村为单位导入。很明显,该接口是没有实现相应的需求的,也存在改进的空间,但是时间紧急,而后续的一系列需求变更导致笔者没有继续沿用同事所写接口,在此基础上改进的想法,当然也和本人的习惯有关(笔者本人不太喜欢改别人的代码,尤其是对没有文档说明,代码中注释量极少的代码很是厌恶。最起码把接口适用场景交代清楚!而leader又指定在别人接口基础修改,在没有任何资料的情况下,很是难受)。该接口很死板,在测试时,Excel表格的字段顺序必须和测试接口swagger页面所填写的顺序一致,一旦出错就会报错。