文章目录
前情
.net8.0需从excel下载500MB以上的Excel。记录一下过程。
Excel数据导出小记[链接],之前记录了三种方式:
1、前端导出:远程服务器数据传输本地过慢
2、后端分页导出:SQLServer版本低分页麻烦,过于依赖建表、清表
3、后端批量导出:NOPI 导出
本文在记录在第三种情况下的小案例。
流程
导出流程分 业务流程、文件流程。
1、业务流程: 请求=> 数据查询=>查询结果(DataReader)=>workbook=>回收ODBC返回结果(查询成功正在下载)
2、文件流程: 查询结果(DataReader)=>workbook=> 保存服务器本地=>移动到共享文件服务器。
思路1、为什么流程分两个?
如果是一个线程,在保存完成之前,odbc的connect和前端request将一直被占用。
不如请求完了,异步保存。
思路2、为什么返回结果之前先将数据写入workbook?
因为 DataReader在 connect关闭后回收。所以在断开数据库之前,必须将数据写入内存,以供之后保存。
思路3、为什么要保存服务器本地?
API服务器(A)、文件服务器(B)
如新建到B,从workbook中写到excel,远程传输很慢,制约于A、B之间的网络正常。
而新建到A,再剪切到B,对网络依赖更低。更稳定。
思路4、为什么不直接保存到我的电脑
数据库、API服务器、文件服务器在同一网段,是局域网内的数据移动。
比api传输JSON,再本地写进EXCEL。效率快一倍有余。
存进共享文件服务器,也可降低同一工作的重复、协作文件传输频率。
数据查询实现
csharp
public static async Task<bool> OutPutExcel(SqlConnection connection, CommandType cmdType, List<CmdData> list, string extra)
{
var workbook = new SXSSFWorkbook(null, 100, true); //NPOI XSSF
//定义路径tempPath 为api服务器临时路径 extra 为前端请求保存路径(文件服务器)
string tempPath = @"C:\wwwroot\ExcelTemp\"; //需设置读写路径读写权限
tempPath += Path.GetFileName(extra);
//1、查询数据
using (SqlCommand cmd = new SqlCommand(list[0].sql, connection))
{
cmd.CommandType = cmdType;
if (list[0].parameter != null) cmd.Parameters.AddRange(list[0].parameter);
cmd.CommandTimeout = timeout; // 超时定义
//1.1 转移数据 到 wookbook
using (DbDataReader reader = await cmd.ExecuteReaderAsync())
{
ExcelHelper.OutPut(reader, tempPath, ref workbook);
}
}
//2、异常存储EXCEL
_ = Task.Run(() => ExcelHelper.SaveExcel(tempPath, workbook, extra));
//3、告知response,数据查询结束,保存正在进行
return true;
}
1、 查询数据 ==> 2、结果存内存 ==> 3、告知前端在下载了,等会你看看结果。
数据导出实现
csharp
using NPOI.SS.UserModel;
using NPOI.XSSF.Streaming;
using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
public class ExcelHelper
{
/// <summary>
/// 1、DbDataReader=>workbook
/// </summary>
public static void OutPut(DbDataReader reader, string filePath, ref SXSSFWorkbook workbook)
{
const int maxRowsPerSheet = 1_000_000; // 每个Sheet最大行数 超过新建sheet
const int bufferSize = 50; // 一次写入条数(优化内存)
// 初始化流式工作簿(关键内存优化)
try
{
int sheetIndex = 1;
ISheet currentSheet = workbook.CreateSheet($"Sheet_{sheetIndex}");
// 创建标题行
IRow headerRow = currentSheet.CreateRow(0);
for (int i = 0; i < reader.FieldCount; i++)
{
headerRow.CreateCell(i).SetCellValue(reader.GetName(i));
}
int rowIndex = 1; // 数据行从1开始(0是标题行)
while (reader.Read())
{
// 达到最大行数时切换Sheet
if (rowIndex >= maxRowsPerSheet)
{
// 正确调用无参数FlushRows(NPOI 2.7.3+)
((SXSSFSheet)currentSheet).FlushRows(); // 刷新当前Sheet[^1]
sheetIndex++;
currentSheet = workbook.CreateSheet($"Sheet_{sheetIndex}");
rowIndex = 1; // 新Sheet重置行索引
}
// 创建数据行
IRow dataRow = currentSheet.CreateRow(rowIndex);
// 写入所有列数据(类型安全处理)
for (int col = 0; col < reader.FieldCount; col++)
{
var cell = dataRow.CreateCell(col);
// 根据数据类型安全写入
if (reader.IsDBNull(col))
{
cell.SetCellValue((string)null);
}
else
{
//字符转换
switch (Type.GetTypeCode(reader.GetFieldType(col)))
{
case TypeCode.String:
cell.SetCellValue(reader.GetString(col));
break;
case TypeCode.DateTime:
cell.SetCellValue(reader.GetDateTime(col));
break;
case TypeCode.Int16:
cell.SetCellValue(reader.GetInt16(col));
break;
case TypeCode.Int32:
cell.SetCellValue(reader.GetInt32(col));
break;
case TypeCode.Int64:
cell.SetCellValue(reader.GetInt64(col));
break;
case TypeCode.Decimal:
cell.SetCellValue((double)reader.GetDecimal(col));
break;
case TypeCode.Double:
cell.SetCellValue(reader.GetDouble(col));
break;
case TypeCode.Single:
cell.SetCellValue(reader.GetDouble(col));
break;
case TypeCode.Boolean:
cell.SetCellValue(reader.GetBoolean(col));
break;
default:
cell.SetCellValue(reader.GetValue(col).ToString());
break;
}
}
}
if (rowIndex % 100000 == 0)
{
_ = Task.Run(() => System.GC.Collect());
}
rowIndex++;
}
}
catch (Exception ex)
{
}
}
/// <summary>
///2、保存 workbook=>Excel
/// </summary>
public static void SaveExcel(string tempPath, SXSSFWorkbook workbook, string truePath)
{
try
{
//保存本地
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
workbook.Write(fs, false);
}
//异步执行剪切
_ = Task.Run(() => ExcelHelper.CutFile(tempPath, truePath));
}
catch (Exception ex)
{
}
finally
{
workbook.Dispose();
System.GC.Collect();
}
}
/// <summary>
///3、 剪切 本地 'a.xlsx'=>远程 '正在执行_a.xlsx'
/// </summary>
public static void CutFile(string fromPath, string truePath)
{
//临时路径
string tempPath=Path.GetDirectoryName(truePath)+"\\正在执行_"+ Path.GetFileName(truePath);
// 检查源文件是否存在
while (!File.Exists(tempPath))
{
Task.Delay(1000); // 减少CPU占用
if (File.Exists(fromPath))
File.Move(fromPath, tempPath);
}
//重命名
while (File.Exists(tempPath)&&(!File.Exists(fromPath)))
{
Task.Delay(1000); // 减少CPU占用
ReName(tempPath, truePath);
}
}
/// <summary>
/// 4、重命名 '正在执行_a.xlsx'=>'a.xlsx'
/// </summary>
public static void ReName(string fromPath, string truePath)
{
File.Move(fromPath, truePath);
}
}
'正在执行_' 前缀,可识别,文件是否完成,以便打开。
1、保存到内存==> 2、保存到iis所在服务器 ==> 3、移动到共享服务器
==> 4、重命名 以表示 传输结束