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

问题描述 投票:0回答:1

我面临的挑战是我有一个由用户创建的 SQL,这个 SQL 我现在通过 XPO(DevExpress,很快就不再直接通过 NPGSQL 进行包装)运行。在那里,我已经将所有数据加载到内存中并将结果转换为我自己的包装类。

现在我使用自己的对象并使用 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
    }
}

我现在可以使用带有 MemoryStream 的 CSVHelper 创建 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 文件写入磁盘上的临时目录然后返回? 例如,我可以在此处访问 S3Bucket,或者使用我自己在 Kubernetes 内的目录。如果我通过文件返回流,我不能在成功传输后简单地删除文件,可以吗?我无法执行最后的操作?我实际上只想传输请求的文件,然后不将其存储在某处。

谢谢!

c# csv memory-management asp.net-core-webapi
1个回答
0
投票

最简单的方法是在本地保存文件并进行流式传输。

或者,您可以尝试直接写入

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"
    };
}
© www.soinside.com 2019 - 2024. All rights reserved.