异步填充DataTable?

17

我在一个.NET Core 2.0应用中有以下函数。

public DataTable CallDb(string connStr, string sql)
{
    var dt = new DataTable();
    var da = new SqlDataAdapter(sql, connStr);
    da.Fill(dt);
    return dt;
}

如何将它转换为异步函数?

public async Task<DataTable> CallDb(string connStr, string sql)
{
    var dt = new DataTable();
    var da = new SqlDataAdapter(sql, connStr);
    da.Fill(dt); // No FillAsync to await?
    return dt;
}

我需要使用 DataTable,因为 SQL 可能会返回具有不同模式的数据。有没有更好的方法来处理动态模式?


你为什么要用 [tag:entity-framework-core] 标记它呢? - Camilo Terevinto
这段代码基本上存在缺陷。它将强制您使用极其不安全的方法来构建SQL字符串,从而使您的程序容易受到SQL注入攻击的威胁。您需要为该方法定义一个额外的参数,以便它能够接受SqlParameter数据。 - Joel Coehoorn
@JoelCoehoorn,安全在我这个特定项目中不是问题。 - ca9163d9
1
首先,这不仅仅是关于安全性的问题。参数还有助于避免像名为O'Brien的人这样的天真数据问题,并且它们有助于提高性能。其次,这种态度会导致其他项目中出现糟糕的代码,当它确实很重要时。参数只是正确的做法。学会正确使用它们并在您的常见数据模式中考虑它们,这样当仅用于娱乐的代码突然变得真实时,您已经在正确的方式下进行操作了。 - Joel Coehoorn
@JoelCoehoorn,我在项目中不得不使用动态 SQL,并且在这个特定的项目中安全性不是问题。 "'" 已经处理好了。 - ca9163d9
显示剩余2条评论
3个回答

31

SqlDataAdapter 从未更新以包括 TPL 版本的方法。您可以这样做:

await Task.Run(() => da.Fill(dt));

但那样做会创建一个没有任何用处的线程。

一个好的方法是使用类似这样的东西:

public async Task<DataTable> CallDb(string connStr, string sql)
{
    var dt = new DataTable();
    var connection = new SqlConnection(connStr);
    var reader = await connection.CreateCommand().ExecuteReaderAsync();
    dt.Load(reader);

    return dt;
}
当然,一些修改如使用`using`语句应该被进行。不过,在这里你正在以正确的方式使用异步调用。

6
虽然这个答案通过在方法中使用另一个await解决了问题,但当大量IO操作发生在“dt.Load”中时,它并没有释放线程。这就是使用异步(async)的巨大好处所在。 - Saeb Amini
2
@SaebAmini - 但是当 SQL Server 处理查询并在开始返回结果之前,它肯定会释放线程吧? 因此有所收益,而且可能相当显著。 - Joe
3
@Joe,没错,你确实会获得一些好处,因为你释放了线程的往返时间+SQL服务器实际处理查询所需的时间。如果您具有高延迟/需要大量时间来处理复杂查询,则该部分可能非常重要。但是我想指出的是,大多数IO通常发生在您实际从读取器中检索结果时,而这仍然是同步执行的dt.Load。要获得该好处,您应该使用SqlDataReader提供的异步方法:NextResultAsyncReadAsync,或创建自己的dt.LoadAsync - Saeb Amini
3
在 GitHub 上有一个问题,涉及向 DataAdapters API 添加异步功能。 https://github.com/dotnet/runtime/issues/22109 - Lachlan Ennis

7

虽然在这种情况下对ExecuteReaderAsync()的初始调用不会阻塞,但是dt.Load(reader)可能执行的是reader.Read()的等效操作而不是await reader.ReadAsync(),并且在检索行时可能会阻塞调用线程。

如果您确实需要一个用于与外部API一起使用或因为不预先知道字段定义而需要完全异步行为的DataTable,那么最好使用自己的代码构建DataTable,根据reader.GetName()reader.GetFieldType()添加所需的列,然后使用循环使用await reader.ReadAsync()dt.Rows.Add()填充它的行。


3

Camilo Terevinto的回答基础上,我想异步处理从异步读取器(DataTable.Load)加载数据表的操作。

我创建了一个SqlCommand扩展,以每次读取一条记录的方式读取数据。虽然数据仍然同步加载,但使用ReadAsync进行异步迭代,每次迭代一条记录。

public static class SqlExtensions
{
    public static async Task<DataTable> FillDataTableAsync(this SqlCommand cmd)
    {
        DataTable dt = null;
        using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
        {

            DataTable schemaTable = await reader.GetSchemaTableAsync();
            dt = new DataTable();
            foreach (DataRow row in schemaTable.Rows)
                dt.Columns.Add(row.Field<string>("ColumnName"), row.Field<Type>("DataType"));


            while (await reader.ReadAsync())
            {
                DataRow dr = dt.Rows.Add();
                foreach (DataColumn col in dt.Columns)
                    dr[col.ColumnName] = reader[col.ColumnName];
            }
        }
        return dt;
    }
}

并且像这样使用它:

DataTable dt = null;
using (SqlCommand cmd = new SqlCommand("SELECT * FROM TABLE", conn))
{
    dt = await cmd.FillDataTableAsync();
}

如果我需要异步更新DataTable,我会先删除记录,然后在事务内使用SqlBulkCopyWriteToServerAsync(dt)方法。

using (SqlCommand cmd = new SqlCommand("DELETE FROM TABLE", transaction.Connection))
{
    cmd.Transaction = transaction;
    await cmd.ExecuteNonQueryAsync();

}
using (SqlBulkCopy copy = new SqlBulkCopy(transaction.Connection, SqlBulkCopyOptions.Default, transaction))
{
    copy.DestinationTableName = "TABLE";
    await copy.WriteToServerAsync(dt);
}

注意:我尚未测试其效率与SqlDataAdapter相比。


比SqlDataAdapter慢四倍?https://social.msdn.microsoft.com/Forums/en-US/9c83e9ef-c663-43d5-a737-18b7a19f4534/get-datatable-async-huge-performance-issue?forum=aspwebapi - TreeAndLeaf
@TreeAndLeaf 我猜这可能是原因 :( 尽管我对这个测试有点怀疑。每次读取都调用await reader.ReadAsync().ConfigureAwait(false),我不确定为什么会这样。 - clamchoda
1
如果不需要复制元数据,您可以通过以下方式加快速度(未经测试):DataTable dt = new(); using (var reader = await sqlComm.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { var values = new Object[reader.FieldCount]; reader.GetValues(values); dt.Rows.Add(values); } } return dt; - TreeAndLeaf
1
如果不需要复制元数据,你可以通过加快速度。像这样(未经测试!):DataTable dt = new(); using (var reader = await sqlComm.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { var values = new Object[reader.FieldCount]; reader.GetValues(values); dt.Rows.Add(values); } } return dt; - undefined
也许使用 dt.BeginLoadData() 和 dt.EndLoadData() 可以提高性能。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接