首页 > 其他分享 >如何设计高效而通用的数据导入框架

如何设计高效而通用的数据导入框架

时间:2022-10-13 14:07:07浏览次数:72  
标签:customer 高效 框架 req token 导入 rsp


概述

导入导出是常见的后台管理应用功能,几乎所有的后台系统都要用到。

借助第三方组件,如POI等,要实现基本的导入导出功能并不难,但是要设计一个高效且通用的导出导入框架,却并不容易。这考验到系统框架人员的设计功力。以笔者在多个公司工作时所见到的现状,未发现一个很好用的框架。导出的功能比较独立还好一点,比较容易做成通用的框架,一般的公司都实现了导出框架,虽然不是尽善尽美,但基本可用。

而涉及到导入方面,基本上没有发现一个十分高效而通用的导入框架。包括不少大公司在内,都没有现成的导入框架,需要开发人员自己一点点的自己写代码完成。而开发人员水平参差不齐,基本上写出来的东西只能说勉强完成了功能,一方面性能方面不太好,更主要的是可扩展性无从谈起。每个开发人员个各搞,经常容易出乱子。

那么为什么一般的公司的平台框架都会提供导出框架而没提供导入框架呢?

这是因为导入比导出要复杂一些,导出一般仅仅是将数据搞出来即可,属于比较独立的技术处理,方便做到系统框架里去。而对于导入功能来讲,并不是纯粹的导入,往往伴随着要尽行数据有效性检查等复杂事情,而数据校验根据具体业务场景可简可繁,实际上是具体的业务规则。这种业务逻辑是不能做到平台框架里的。所以大多数公司的平台框架都不提供导入功能,有的即使提供了也不太好用。还是需要自己写代码处理。

那么导入能不能做成通用的功能做到平台框架里呢?实际上是可以的。


设计思路

设计要点就是,将导入过程切分成几步:

  • 第1步——保存原始数据到临时区,
  • 第2步——在临时区检查数据,也就是数据校验,得到检查结果信息。
  • 第3步——将有效数据导入正式区,将检查出的校验结果信息传出来让用户看到。

其中第1步是纯技术的事情,由框架中台代码实现。第2步,第3步由开发人员在后台数据层实现,系统框架提供一些约定的信息处理规则即可。

中台框架定好后,供前端调用的接口也就确定下来了。

需要注意的是,业务校验功能是不能设计在中台的java代码里的,否则框架结构会非常难处理,校验功能一定要放到数据库层。

了解了这个思路后我们再看下实现代码。

前端代码示例

先说前台:前台代码分2部分,一部分是前端开发人员使用的调用部分示例,在customer.js文件里,一部分是前端框架部分,在app.js文件里。

前端采用ajax异步方式处理上传。

前台第1部分:

前端调用代码示例


function importChooseFile() {
if (isDblClick()) return;
$("#up_btn").click(function() {
importCustomer();
});
$("#myUploadDialog").modal();
}

function importCustomer() {
$("#d2").load("commons.html #myImportResult");
if (isDblClick()) return;

var cols = {
customerid: "Customer Id",//db field: excel header name
firstname: "First Name",
lastname: "LastName",
company: "Company",
address: "Address",
city: "City",
state: "State",
country: "Country",
postalcode: "Zip Code",
phone: "Phone",
fax: "Fax",
email: "Email",
supportrepid: "Support Rep Id"
};

var file1 = $("#myfile")[0].files[0];
var formData = new FormData();
formData.append("file1", file1);
formData.append("method", "sp_imp_customer");
formData.append("verifyMethod", "sp_imp_customer_verify");
formData.append("cols", JSON.stringify(cols));//上传文件时表单变量不支持复杂结构,转为字符串传。
uploadImport(formData);
}

先看调用部分: 前端传送数据用jquery封装到表单里。

fileupload: 要传的文件,这里只以一个文件为例子。

cols 定义Excel文件表头对应的数据字段。这个好理解。注意这里提暴露给前端开发人员的接口规范只需要定义好名称对应关系,并不需要管位置,位置应当由框架去处理。

method:后台导入的SP名称

verifyMethod: 后台校验数据的SP名称

我们看到,前端的调用部分比较简单的。

如何设计高效而通用的数据导入框架_Excel导入

前端的框架部分是自己提炼出来的,也比较简单,主要添加一些需要传到后台的通用属性,比如token,userCode等,这些由前端的框架app.js处理,这部分数据框架范畴,这部分代码可以重用,一般不用动它。

前端框架部分示例

function uploadImport(formData) {

formData.append("userCode", loginInfo.userCode);
formData.append("roleCode", loginInfo.roleCode);
formData.append("tag", "IMPORT");
formData.append("token", "" + new Date().getTime());//required for import

confirm1('Import data', 'Import selected file into database. Are you sure?', function() {
$("#myUploadDialog").modal('hide'); //close dialog manually
$(document.body).css("cursor", "progress");
$.ajax({
url: 'importserver',
type: 'post',
data: formData,
contentType: false,
processData: false,
success: function(result, status) {
console.log("---ajax-result=" + result);
$(document.body).css("cursor", "default");
proccessResult(JSON.parse(result));
},
error: function(xhr) {
$(document.body).css("cursor", "default");
console.log("---ajax-Error:" + xhr.status + " " + xhr.statusText);
}
});
});
}


中台实现

中台部分由Java实现,当然设计方案确定后实际上也可以用任何其它语言实现。因为笔者对java稍微熟悉点,这里以Java为例说明。

虽然中台部分属于此框架的核心,但是都属于系统框架的内容,可以不用给开发人员开放。这里对中台代码简要说明下。

先看控制器ImportController,

控制器代码示例


@RestController
public class ImportController {
private static Logger log = LoggerFactory.getLogger(ImportController.class);

@Autowired
private ImportDao importDao;

@RequestMapping(value = "/importserver")
public String importPost(@RequestParam("file1") MultipartFile f1,
@RequestParam("method") String method,
@RequestParam("verifyMethod") String verifyMethod,
@RequestParam("tag") String tag,
@RequestParam("token") String token,
@RequestParam("cols") String cols,
@RequestParam("userCode") String userCode,
@RequestParam("roleCode") String roleCode,
HttpServletRequest request) throws IOException {
long t0 = System.currentTimeMillis();
ObjectMapper mapper = new ObjectMapper();
ImportRequest req = new ImportRequest();
req.method = method;
req.verifyMethod = verifyMethod;
req.token = token;
req.tag = tag;
req.cols = mapper.readValue(cols, VOS.class);
req.userCode = userCode;
req.roleCode = roleCode; // optional

ImportResponse rsp = req.copy();

String today = new SimpleDateFormat("yyyyMMdd").format(new Date());
String currDir = request.getServletContext().getRealPath("/");
String fileUrl = "upload/" + today + "/" + U.getTempFileName(f1.getOriginalFilename());
String destFilePath = currDir + fileUrl;
log.debug("upload original file=" + f1.getOriginalFilename());
log.debug("upload destFilePath=" + destFilePath);
File f = new File(destFilePath);
f.getParentFile().mkdirs();
f1.transferTo(f);

req.fullPath = destFilePath;

rsp = importDao.execute(req);
long t1 = System.currentTimeMillis();
rsp.sourceFile = f1.getOriginalFilename();
rsp.fileUrl = fileUrl;
rsp.consumed = t1 - t0;
String str = mapper.writeValueAsString(rsp);
return str;
}
}

主要用途是接住前端传送的内容,将上传的文件保存为临时文件以便下一步导入,其它的参数封装成一个ImportRequest对象,

都是Spring 的一些常规处理,比较好理解。


通用的DAO实现-第1部分

核心部分是DAO层 ImportDao

我们看到,DAO层主要分2步,第1步,将数据保存到临时表,具体的插入动作是调用 前台传出的方法名,req.method


@Component
public class ImportDao extends BaseDao<ImportRequest, ImportResponse> {

private static Logger log = LoggerFactory.getLogger(ImportDao.class);
private static final String PARAM_ROWNUM = "p_rownum";

@Override
public ImportResponse execute(ImportRequest req0) {

ImportRequest req = (ImportRequest) req0;
ImportResponse rsp = req.copy();
try {
init(req.method);
Workbook wb = new XSSFWorkbook(new File(req.fullPath));
Sheet sheet1 = wb.getSheetAt(0);
Row row = sheet1.getRow(0);

initExcelIndex(req.cols, row);
for (ProcedureColumn pc : spCols) {
String dbColName = pc.COLUMN_NAME.substring(2);
if (!req.cols.containsKey(dbColName) && (!"token".equals(dbColName)) && (!"rownum".equals(dbColName))) {
String ss = "Error: SP parameter not defined in front end,SP=" + req.method + ",dbColName= "
+ pc.COLUMN_NAME;
log.error(ss);
throw new Exception(ss);
}
}
int n = 0;
conn.setAutoCommit(false);// Important!
for (int i = 1; i <= sheet1.getLastRowNum() + 1; i++) {
row = sheet1.getRow(i);
if (row == null) {
continue;
}

for (ProcedureColumn pc : spCols) {
Object val = null;

Cell cell = row.getCell(pc.excelIndex);
CellType tp = cell.getCellType();
String key = pc.COLUMN_NAME;
if (key.equals(PARAM_TOKEN)) {
val = req.token;
} else if (key.equals(PARAM_ROWNUM)) {
val = i;
} else if (tp == CellType.NUMERIC) {
val = cell.getNumericCellValue();
} else if (tp == CellType.STRING) {
val = cell.getStringCellValue().trim();
}
stmt.setObject(pc.pos, val);
}
stmt.addBatch();
n++;
if (i % 1000 == 0) {
stmt.executeBatch();
stmt.clearBatch();
conn.commit();
n = 0;
}
}
if (n > 0) {
stmt.executeBatch();
conn.commit();
}
wb.close();
conn.setAutoCommit(true);
executeImportVerify(req, rsp);
} catch (Exception e) {
rsp.result = C.RESULT_FAIL;
rsp.message = e.getMessage();
log.error(e.getMessage(), e);
}
return rsp;
}

中台的作用主要是读取EXCEL文件。注意为了提高效率,我们没有用spring封装的数据访问对象,而是直接用的jdbc底层方法。只用到spring的datasource得到Connection

同时,批量提交十分重要。

需要注意的是,由于要做成通用的框架,中台的导入代码,不能涉及到任何具体的业务字段名。

initExcelIndex(req.cols, row);这句话就是做这个用的,填充入参时也是通用的代码。针对用户给定的数据做循环。

具体的字段名,只在前端/后端涉及到。一般在实际开发过程中由前后端开发人员沟通而定。


通用的DAO实现-第2部分

Dao第2步,检查数据,调用前端传入的方法名,req.verifyMethod, 调用数据层SP检查数据有效性。

并将错误信息写入到保存的临时Excel文件里,后面供用户查看。

private void executeImportVerify(ImportRequest req, ImportResponse rsp) throws Exception {

try {
stmt = conn.prepareCall("{call " + req.verifyMethod + "(?,?)}");// fixed SP name with suffix _verify
stmt.setObject(1, req.token);// fixed parameter
stmt.setObject(2, req.userCode);// fixed parameter
stmt.executeQuery();

rs = stmt.getResultSet();
List<VO> list = U.getDataFromResultSet(rs);

FileInputStream fis = new FileInputStream(req.fullPath);
XSSFWorkbook wb = new XSSFWorkbook(fis);
XSSFSheet sheet1 = wb.getSheetAt(0);
XSSFRow row0 = sheet1.getRow(0);
int n = sheet1.getLastRowNum();
rsp.totalNum = n;
rsp.successNum = n;

CellStyle style = wb.createCellStyle();
Font font1 = wb.createFont();
font1.setFontName("Arial");
font1.setFontHeightInPoints((short) 11);
style.setFont(font1);
style.setFillForegroundColor(IndexedColors.RED1.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);

if (rs != null && list.size() > 0) {
int ncol = row0.getLastCellNum();
for (VO vo : list) {
int rownum = Integer.valueOf(vo.get("rownum").toString());
Row row = sheet1.getRow(rownum);
Cell cell = row.createCell(ncol);
cell.setCellValue(vo.get("error_message").toString());
for (int i = 0; i < ncol + 1; i++) {// need set every cell instead of setRowStyle
row.getCell(i).setCellStyle(style);
}
}
rsp.errorNum = list.size();
rsp.successNum = rsp.totalNum - rsp.errorNum;
}
rsp.message = "Total=" + rsp.totalNum + ", success=" + rsp.successNum + ", error=" + rsp.errorNum;
File file = new File(req.fullPath);
try (OutputStream fileOut = new FileOutputStream(file)) {
wb.write(fileOut);
;
}
fis.close();
wb.close();
rsp.result = C.RESULT_SUCCESS;
} catch (Exception e) {
rsp.result = C.RESULT_FAIL;
rsp.message = e.getMessage();
log.error(e.getMessage(), e);
throw e;
} finally {
close();
}
}

DAO的第2步比较重要,只有在这一步才将数据正式写入,并将结果信息传递给用户查看。

这里要特别注意,中台代码无论是Controller和DAO, 始终贯彻设计理念,并没有任何涉及业务处理相关的代码。


后台代码

这里说得后台代码,说的是数据库层。由具体的后端开发人员实现。包含2个存储过程,一个是导入临时表:

这里是示例SP名为 sp_imp_customer

后台代码示例-插入临时表

CREATE DEFINER=`root`@`localhost` PROCEDURE `test`.`sp_imp_customer`(
IN `p_token` varchar(50),
IN `p_rownum` int,
IN `p_customerid` int,
IN `p_firstname` varchar(40),
IN `p_lastname` varchar(20),
IN `p_company` varchar(80),
IN `p_address` varchar(70),
IN `p_city` varchar(40),
IN `p_state` varchar(40),
IN `p_country` varchar(40),
IN `p_postalcode` varchar(20),
IN `p_phone` varchar(20),
IN `p_fax` varchar(20),
IN `p_email` varchar(60),
IN `p_supportrepid` int
)
COMMENT 'Import customer info into temp table "customer_import", p_token passed from front end, p_rownum(excel row num) passed from middle tier of java framework.'
begin


insert
into
test.customer_import
(token,
rownum,
customerid,
firstname,
lastname,
company,
address,
city,
state,
country,
postalcode,
phone,
email,
supportrepid)
values
(p_token,
p_rownum,
p_customerid,
p_firstname,
p_lastname,
p_company,
p_address,
p_city,
p_state,
p_country,
p_postalcode,
p_phone,
p_email,
p_supportrepid);

END

代码很简单,就是基本的导入,注意临时表需要比原表新增2个字段,token, rownum,error_message


  • token是前端传入的唯一导入号,
  • rownum是要存入源文件行号,以便后面回写错误信息。
  • error_message 错误信息,供第2步校验时用。

只需要写一条SQL语句即可,供中台调用。


后台代码示例-数据校验及处理

第2个是校验SP,这里示例SP名为  sp_imp_customer_verify



CREATE DEFINER=`root`@`localhost` PROCEDURE `test`.`sp_imp_customer_verify`(
IN `p_token` varchar(50),
IN `p_usercode` varchar(50)
)
COMMENT 'Checking imported data in the temp table with specific business rule; record error info; import valid data to formal table "customer", p_token should be passed'
begin

-- 开发人员自己加入校验业务逻辑,记录错误信息
update test.customer_import
set error_message = 'Missing First Name'
where token = p_token
and firstname is null;

update test.customer_import
set error_message = 'Missing Last Name'
where token = p_token
and lastname is null;

delete from test.customer_import
where datediff(now(), dw_create_time) > 30;


insert into test.customer
(firstname,
lastname,
company,
address,
city,
state,
country,
postalcode,
phone,
fax,
email,
supportrepid,
create_by)
select firstname,
lastname,
company,
address,
city,
state,
country,
postalcode,
phone,
fax,
email,
supportrepid,
p_usercode
from test.customer_import
where token = p_token
and error_message is null;

select *
from test.customer_import
where token = p_token
and error_message is not null;

END

这里根据业务规则先批量检查数据

将错误信息写入字段error_message

将有效的数据写入正式表。

注意中台一定要传入参数 p_token

总结

我们看到,导入框架的核心思路就是分层分步骤,将业务部分剥离,核心部分放在中台,但是作为系统框架,对开发人员屏蔽了复杂性,

只需要在两端实现基本的业务功能即可:前端开发人员声明具体用到的字段清单,后台开发人员在数据层编写业务校验规则,以及很简单的写入操作。

该框架经测试,20个字段以内,导出速度大于1万条每秒, 导入速度大于5千条每秒。


源代码说明


全部源代码地址:

​https://github.com/chenyh-a/easyweb​

源代码结构

如何设计高效而通用的数据导入框架_应用框架_02

示例数据导入

下载源代码后, 主目录下有一个数据文件,将它导入mysql或者mariaDB 数据库:

进入mysql客户端命令窗口

创建数据库 test

然后导入数据文件 source E:\git\easyweb\data.sql改为你自己的具体路径

修改配置文件 application.properties 的数据库连接参数


编译与打包

打包 mvn package (假定你已经配置好了maven环境)

生成WAR包 easyweb-0.0.1.war

如何设计高效而通用的数据导入框架_Excel导入_03

将生成在target目录下的war包改名为 easyweb.war 放入tomcat 的webapps目录下,启动tomcat即可访问

​http://localhost:8080/easyweb/customer_list.html​​ 

查看效果: 

如何设计高效而通用的数据导入框架_Excel导入_04

如何设计高效而通用的数据导入框架_应用框架_05

或者将源代码整体导入IDE 环境,比如eclipse等。在IDE里运行即可看到效果(IDE必须预先配置好了Spring支持环境

如果对源代码有任何疑问或发现错误疏漏请不吝指正。



标签:customer,高效,框架,req,token,导入,rsp
From: https://blog.51cto.com/u_15817067/5753297

相关文章

  • ThreadX视频教程第3期:ThreadX工程模板框架,启动执行流程以及STM32CubeMX生成ThreadX工
    ​​​​ 本期视频教程主要是把ThreadX的工程模板框架和启动执行流程做个说明,并使用STM32CubeMX给大家做个工程模板生成展示。如果大家想使用我们工程的创建方法,含MDKAC......
  • 导入导出Csv
    导入Csv/***导入Csv*ThinkPHP5.1框架*/publicfunctioncsvDr(){$excelData=array();//第一种:将文件一次性......
  • 004——剪辑视频前 , 该如何正确的导入素材
    剪辑视频前,该如何正确的导入素材常用的导入格式导入方式直接拖入从菜单栏中选择………………………………......
  • 【STM32H7教程】第12章 STM32H7的HAL库框架设计学习
    第12章      STM32H7的HAL库框架设计学习通过本章节,主要是想让大家对HAL库程序设计的基本套路有个了解,防止踩坑。12.1初学者重要提示12.2HAL库的配置文件12.3HAL......
  • 【安富莱二代示波器教程】第4章 示波器设计—主界面布局和框架
     第4章       示波器设计—主界面布局和框架本章简单为大家说明主界面的布局和框架。4.1主界面框架选择4.2主界面布局4.3         总结  4.1 主界......
  • Java集合框架
    什么是集合概念:对象的容器,定义了对多个对象进行操作的常用方法。可以实现数组的功能和数组的区别:数组长度固定,集合长度不固定数组可以存储基本类型和引用类型,集合只......
  • 【RL-TCPnet网络教程】第21章 RL-TCPnet之高效的事件触发框架
    第21章      RL-TCPnet之高效的事件触发框架本章节为大家讲解高效的事件触发框架实现方法,BSDSocket编程和后面章节要讲解到的FTP、TFTP和HTTP等都非常适合使用这种......
  • Vue——前端框架
    Vue    Vue快速入门  <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>Title</title></head><body><divid="app">......
  • Python相对路径导入问题
    如果某个项目的文件结构如上,想要在f1.py中导入pkg包的时候,可能会这样写:from....importpkg但是很遗憾,这样会引发ImportError异常。直接运行f1.py时,异常信息是Import......
  • 【框架】SSM整合
    SSM整合基础整体知识架构SSM整合表现层数据封装异常处理器项目异常处理方案案例:SSM整合标准开发SSM整合SSM整合流程创建工程SSM整合SpringSpringConfig......