背景描述:数据库里面有一张表A,里面有90w+的数据,有时候会达到上百万。我想从前端页面通过点击“导出”按钮导出数据。
面临的问题及思路:
1.从数据库中查90w+数据时间太久,会导致前端超时,用户体验不好
2.一张表存90w+数据不现实,必定要拆分
3.每次用户导出都要查询数据库→下载,用户体验不好,所以第一次生成文件以后把文件放到文件服务器上,然后返回文件地址,以后每次导出只需要通过地址下载存在文件服务器上的文件即可
流程图如下:
代码简要:
1.Controller层
@ApiOperation("导出")
@GetMapping("output/exports")
@ResponseBody
@PreAuthorize("isAuthenticated()")
Output OutputsExport(HttpServletResponse response,Input Input) throws IOException {
//查表,看有没有地址
DataDownloadUrl dataDownloadUrl=dataDownloadUrlRepository.findByName("xxx");
if(如果数据库中有地址){
return 地址;
} else{
//起一个线程,用于生成excel。另一方面先返回给用户一个状态
Thread ts=new Thread(new Runnable() {
@Override
public void run() {
try {
//调用Service层
service.excelOutput(Input,prefix); //prefix是上传文件服务器所要用到的东西
} catch (IOException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
});
ts.start();
return new Output("正在准备数据,由于数据量较大,请于30分钟之后进行下载!");
}
}
2.Service层(加粗加下划线标红的方法后续列出来)
public Output excelOutput(Input Input,String prefix) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Integer count=mydataRepository.countNum(); //计算总共要导出的数据量 Integer pageSize=100000; //每一个excel存的数据量 //根据数据总量以及设定好的每个excel存的数据量计算excel个数 Integer excelNum=((count%pageSize==0)?(count/pageSize) : (count /pageSize + 1)); Integer writeSize=1000; //分批写入数据量为count的总数据,每次向excel中写入的数量为writeSize Integer writeNum=pageSize/writeSize; //计算每个excel的写入次数 //标题 String[] titles={"主键ID","姓名","学号","成绩"}; String path = "学生信息"+sdf.format(new Date()); //定义保存excel的文件夹 for(int i=0;i<excelNum;i++){//循环生成excel文件 String fileName = "学生信息"+sdf.format(new Date())+"(" + (i+1) + ").xlsx"; //给每个excel命名 SXSSFWorkbook wb=DataDownloadUtil.createWord2007(titles); //创建excel,并把表头写好 SXSSFSheet sheet=wb.getSheetAt(0);//拿到第一个sheet for(int j=1;j<=writeNum;j++){ //循环向excel中写入数据,每个excel的数据量为pageSize int startRow=(j-1)*writeSize+1; //每次获取的起始行 int endRow=startRow+writeSize-1; //每次获取的最后一行 //分页查询,每次查询1000条数据 Page<MyDataPrice> myDatas= myDataRepository.findAll(new PageRequest(writeNum*i+j-1,1000)); List<MyData> myDataList=myDatas.getContent();//Page转List if(!ObjectUtils.isEmpty(myDataList)){ //写入excel for(int x=startRow;x<=endRow;x++){ SXSSFRow eachDataRow = sheet.createRow(x); if ((x-startRow) < myDataList.size()) { MyData excelData = myDataList.get(x - startRow); eachDataRow=MyDataEncap.ExcelOutputTransfer(eachDataRow,excelData); } } } } //for循环结束 //把生成的excel文件命好名以后保存到我们之前定义的文件夹中 DownloadUtil.downLoadExcelToLocalPath(wb, path+ "/"+fileName, i == excelNum - 1 ? true : false); } //把文件夹打包 ZipUtils.doCompress(path, "学生信息"+sdf.format(new Date())+".zip");
//zip文件转流,准备上传至文件服务器 File file=new File("学生信息"+sdf.format(new Date())+".zip"); InputStream inputStream = new FileInputStream(file); //定义一个字符串,用于接收文件服务器返回的地址 String fileUrlFromFileOS = ""; byte[] byt = new byte[inputStream.available()]; inputStream.read(byt); ByteArrayInputStream bis = new ByteArrayInputStream(byt); //丢到服务器上面去,就是下面的save()方法,用fileUrlFromFileOS接收地址 fileUrlFromFileOS = prefix+"/"+new ElianUploadUtil().save(bis); if(fileUrlFromFileOS!=null){ String arr[]= fileUrlFromFileOS.split(":"); fileUrlFromFileOS="https:"+arr[1]; } //下面是将地址保存到数据库中。可以忽略不用看了 if(!ObjectUtils.isEmpty(dataDownloadUrl)){ dataDownloadUrl.setUpdatetime(mydataPrice.getUpdatetime()); dataDownloadUrl.setUrl(fileUrlFromFileOS); dataDownloadUrl.setStatus(0); dataDownloadUrlRepository.save(dataDownloadUrl); }else{ DataDownloadUrl dataDownloadUrl1=new DataDownloadUrl (); dataDownloadUrl1.setUrl(fileUrlFromFileOS); dataDownloadUrl1.setUpdatetime(mydata.getUpdatetime()); dataDownloadUrl1.setModuleName("studentInfo"); dataDownloadUrl1.setStatus(0); dataDownloadUrl1Repository.save(dataDownloadUrl1); } System.out.println(fileUrlFromFileOS); return new Output(); }
3.service里用到的方法
(1)createWord2007
public static SXSSFWorkbook createWord2007(String[] titles) {
// 在内存当中保持 100 行 , 超过的数据放到硬盘中在内存当中保持 100 行 , 超过的数据放到硬盘中
SXSSFWorkbook wb = new SXSSFWorkbook(100);
// 根据总记录数创建sheet并分配标题
SXSSFSheet sheet = wb.createSheet();
SXSSFRow headRow = sheet.createRow(0);
for (int j = 0; j < titles.length; j++) {
SXSSFCell headRowCell = headRow.createCell(j);
headRowCell.setCellValue(titles[j]);
}
return wb;
}
(2)ExcelOutputTransfer
public static SXSSFRow ExcelOutputTransfer(SXSSFRow eachDataRow,MyData myData) {
eachDataRow.createCell(0).setCellValue(myData.getQyid()); //"主键ID"
eachDataRow.createCell(1).setCellValue(myData.getStddrugidnew()); //"姓名"
eachDataRow.createCell(2).setCellValue(myData.getProductname()); //"学号"
eachDataRow.createCell(3).setCellValue(myData.getDrugform()); //"成绩"
return eachDataRow;
}
(3)downLoadExcelToLocalPath
public static void downLoadExcelToLocalPath(SXSSFWorkbook wb, String exportPath,boolean end) {
FileOutputStream fops = null;
try {
fops = new FileOutputStream(exportPath);
wb.write(fops);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(end){
if (null != wb) {
try {
wb.dispose();
} catch (Exception e) {
e.printStackTrace();
}
}
if (null != fops) {
try {
fops.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
(4)doCompress(这里直接放zipUtil工具类。百度都能百度到的)
public class ZipUtils {
private ZipUtils(){
}
public static void doCompress(String srcFile, String zipFile) throws IOException {
doCompress(new File(srcFile), new File(zipFile));
}
/**
* 文件压缩
* @param srcFile 目录或者单个文件
* @param zipFile 压缩后的ZIP文件
*/
public static void doCompress(File srcFile, File zipFile) throws IOException {
ZipOutputStream out = null;
try {
out = new ZipOutputStream(new FileOutputStream(zipFile));
doCompress(srcFile, out);
} catch (Exception e) {
throw e;
} finally {
out.close();//记得关闭资源
}
}
public static void doCompress(String filelName, ZipOutputStream out) throws IOException{
doCompress(new File(filelName), out);
}
public static void doCompress(File file, ZipOutputStream out) throws IOException{
doCompress(file, out, "");
}
public static void doCompress(File inFile, ZipOutputStream out, String dir) throws IOException {
if ( inFile.isDirectory() ) {
File[] files = inFile.listFiles();
if (files!=null && files.length>0) {
for (File file : files) {
String name = inFile.getName();
if (!"".equals(dir)) {
name = dir + "/" + name;
}
ZipUtils.doCompress(file, out, name);
}
}
} else {
ZipUtils.doZip(inFile, out, dir);
}
}
public static void doZip(File inFile, ZipOutputStream out, String dir) throws IOException {
String entryName = null;
if (!"".equals(dir)) {
entryName = dir + "/" + inFile.getName();
} else {
entryName = inFile.getName();
}
ZipEntry entry = new ZipEntry(entryName);
out.putNextEntry(entry);
int len = 0 ;
byte[] buffer = new byte[1024];
FileInputStream fis = new FileInputStream(inFile);
while ((len = fis.read(buffer)) > 0) {
out.write(buffer, 0, len);
out.flush();
}
out.closeEntry();
fis.close();
}
}
(5)save(上传文件到文件服务器上,这里也直接贴类的代码了)
public class ElianUploadUtil {
static Logger logger = LoggerFactory.getLogger(ElianUploadUtil.class);
@Value("${xxxxxxxx}")
private String endpoint="xxxxxxxxxx";
@Value("${xxxxxxxxxx}")
private String accessKeyId="xxxxxxxxxx";
@Value("${xxxxxxxxxx}")
private String accessKeySecret="xxxxxxxxxx";
@Value("${xxxxxxxxxx}")
private String bucketName="xxxxxxxxxx";
@Value("${xxxxxxxxxx}")
private String bucketNameTwo="xxxxxxxxxx";
@Value("${xxxxxxxxxx}")
private String prefix="";
//上面的参数我用xxxx代替了,具体的可以自行百度看一下文件服务器都是怎么配置的
public String save(ByteArrayInputStream byteArrayInputStream) throws Exception {
// 1.创建OSSClient实例
OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
// 2. 上传后文件名
StringBuilder filename= new StringBuilder("MyData");
String key =filename+".zip";
// 3.上传文件
ossClient.putObject(bucketName, key, byteArrayInputStream);
// 4 关闭client
ossClient.shutdown();
return prefix.concat(key);
}
//转流文件
public static ByteArrayInputStream workbookConvertorStream(SXSSFWorkbook workbook) {
ByteArrayInputStream in = null;
try{
//临时缓冲区
ByteArrayOutputStream out = new ByteArrayOutputStream();
//创建临时文件
workbook.write(out);
byte [] bookByteAry = out.toByteArray();
in = new ByteArrayInputStream(bookByteAry);
}
catch (Exception e){
e.printStackTrace();
}
return in;
}
}
注:以上代码仅仅是个人在项目中遇到瓶颈进行的技术记录。也许有更好的导出方案,但是个人水平有限,仅能做到这些。且上面有很多技术点我还没有吃透,所以仅仅做个记录,不敢深入分析乱点江山。