如何在将大文件从数据库导出为CSV格式时保存内存?

pkbketx9  于 12个月前  发布在  其他
关注(0)|答案(2)|浏览(105)

我面临的挑战是,我有一个由用户创建的SQL,这个SQL现在通过XPO(DevExpress,很快就没有 Package 器,直接通过NPGSQL)运行。在那里,我已经将所有数据加载到内存中,并将结果转换到我自己的 Package 器类中。
现在我使用自己的对象并使用CSVHelper从它创建CSV文件。然后我通过WebAPI将这个文件返回给用户。
由于SQL查询的大小可以是1-2GB,因此内存消耗会大幅增加。
预防这种情况的最好方法是什么?
我以前没有做过太多关于流的工作,目前正在阅读它们。如果我理解正确的话,MemoryStream并没有给我带来太多,因为那里的数据也直接加载到内存中。

[HttpGet("export/data")]
public async Task<IActionResult> ExportData(Guid sqlId)
{
    this.OpenConn(); //opens the connection

    string sql = this.GetSql(sqlId);

    using (NpgsqlCommand command = new NpgsqlCommand(sql, conn))
    {
        int val;
        NpgsqlDataReader reader = command.ExecuteReader();
        while (reader.Read())
        {
            // Logic to create the csv file
        }

        this.CloseConn(); //close the current connection
    }
}

我现在可以使用CSVHelper和MemoryStream创建CSV文件,然后返回MemoryStream进行下载。

var ms = new MemoryStream();
var streamWriter = new StreamWriter(ms, Encoding.UTF8);
var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture);

// Logic to create the csv file, with the reader from npgsql
// [...]

return File(ms, "text/csv", "export.csv");

如果我理解正确的话,这并没有给我带来很多好处,因为我已经将数据存储在MemoryStream中,因此内存无论如何都会被填满。处理这个问题的最好办法是什么?将CSV文件写入磁盘上的临时目录,然后返回它?例如,我可以在这里访问S3 Bucket,或者在Kubernetes中使用我自己的目录。如果我通过文件返回流,我不能在成功传输后简单地删除文件,可以吗?我不能做最后的决定吗?实际上,我只想为请求传输文件,而不是将其存储在某个地方。
谢谢你,谢谢

svgewumm

svgewumm1#

最简单的方法是将文件保存在本地并进行流式传输。
或者,你可以尝试直接写入Response.Body(不要忘记刷新)或使用Stephen Cleary的FileCallbackResult方法:

public class FileCallbackResult : FileResult
{
    private Func<Stream, ActionContext, Task> _callback;

    public FileCallbackResult(string contentType, Func<Stream, ActionContext, Task> callback)
        : base(contentType)
    {
        if (callback == null)
            throw new ArgumentNullException(nameof(callback));
        _callback = callback;
    }

    public override Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        var executor = new FileCallbackResultExecutor(context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>());
        return executor.ExecuteAsync(context, this);
    }

    private sealed class FileCallbackResultExecutor : FileResultExecutorBase
    {
        public FileCallbackResultExecutor(ILoggerFactory loggerFactory)
            : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))
        {
        }

        public Task ExecuteAsync(ActionContext context, FileCallbackResult result)
        {
            SetHeadersAndLog(context, result, null, false);
            return result._callback(context.HttpContext.Response.Body, context);
        }
    }
}

和样品用法:

[HttpGet("data")]
public async Task<IActionResult> ExportData()
{
    return new FileCallbackResult("text/csv", async (outStream, context) =>
    {
        await using var sw = new StreamWriter(outStream, leaveOpen: true);
        await using var csvWriter = new CsvWriter(sw, CultureInfo.InvariantCulture, true);
        for (int i = 0; i < 100; i++)
        {
            await Task.Delay(10);
            csvWriter.WriteRecord(new {Int = i, Text = "Qww" + i});
            await csvWriter.NextRecordAsync();
        }
    })
    {
        FileDownloadName = "qwerty.csv"
    };
}

但总的来说,如果可能的话,我建议重新考虑这种方法。如果这是某个ETL管道的导出-例如,您可以切换到仅将文件上传到某个共享位置。

gpnt7bae

gpnt7bae2#

我维护了一个nuget包,可以让这很容易:Sylvan.AspNetCore.Mvc.Formatters.Csv
它使用Sylvan.Data.Csv库实现了CSV的MVC格式化程序。配置MVC服务时,在启动时注册格式化程序:

builder.Services.AddControllersWithViews(opts => { opts.AddSylvanCsvFormatters(); });

在控制器操作中,您可以直接返回DbDataReader,并使用Produces属性指定“text/csv”作为内容类型:

[HttpGet("export/data")]
[Produces("text/csv")]
public async Task<DbDataReader> ExportData()
{
    // no "using" here, connection will be disposed when the reader is closed.
    DbConnection conn = GetConn(); // can be any ADO data provider.
    await conn.OpenAsync();
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "select * from MyData";
    // configure the reader to close the connection when complete.
    return await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection);
}

该实现在CPU和内存使用方面都非常高效,并且完全异步。响应将被流式传输到客户端,并且不会在内存中完全缓冲。如果使用EFCore或其他ORM,也可以返回IEnumerable<T>IAsyncEnumerable<T>,而不是DbDataReader
Sylvan.AspNetCore.Mvc.Formatters.Excel包提供了一个类似的格式化程序,可以返回Excel(.xlsx)文件。这需要添加包并注册格式化程序opts.AddSylvanExcelFormatters()
然后,您可以添加第二个端点,以允许将结果作为Excel文件获取:

[HttpGet("export/data/excel")]
[Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
public async Task<DbDataReader> ExportExcel()
{
    // defer to the other endpoint implementation
    return await ExportData();
}

.xlsx响应必须被缓冲,并且由于文件格式的结构而不能完全流式传输。但是,响应有效负载可能比CSV小得多,因为xlsx文件格式是压缩的,并且重用共享字符串。

相关问题