从 IEnumerable<T> 转换为 DataTable

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

在这被标记为重复之前,我已经看到了很多像这样的答案 将 IEnumerable 转换为 DataTable 并尝试以创建扩展方法的方式执行类似的操作。我问我的问题是因为我遇到的问题可能出在其他地方。

本质上我有相当大的

IEnumerable<T>
(大约16 - 1700万个项目),到目前为止我还没有真正遇到任何问题,直到我尝试使用扩展方法将其转换为
DataTable

/// <summary>
/// Converts IEnumerable to datatable. Mainly for use when using SQLBulkCopy/>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection"></param>
/// <param name="customColumnOrder">Custom order for columns allows me to make sure that the order of columns will always be the same. Am open for suggestions for better ways to do this</param>
/// <returns></returns>
public static DataTable ToDataTable<T>(this IEnumerable<T> collection, List<Tuple<string, int, int>> customColumnOrder)
{
    DataTable dt = new DataTable();
    var type = collection.First().GetType();

    foreach (var column in customColumnOrder)
    {
        dt.Columns.Add(column.Item1, Nullable.GetUnderlyingType(type.GetProperty(column.Item1).PropertyType) ?? type.GetProperty(column.Item1).PropertyType);
    }

    // Populate the table
    foreach (T item in collection)
    {
        DataRow dr = dt.NewRow();
        dr.BeginEdit();

        foreach (var column in customColumnOrder)
        {
            dr[column.Item1] = type.GetProperty(column.Item1).GetValue(item) ?? DBNull.Value;
        }

        dr.EndEdit();
        dt.Rows.Add(dr);
    }

    return dt;
}

这对于包含大约 100,000 个项目的较小表格来说效果很好,但当它达到数百万个项目时就开始变得非常困难。我只是不断地有时间休息。是否有更有效/通常更好的方法从

IEnumerable<T>
转换为
DataTable

我正在转换为

DataTable
,以便我可以使用
SqlBulkCopy
将数据导入数据库。

编辑:这是数据从

传递的地方
    /// <summary>
    /// SqlBulkCopy for saving large amounts of data
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataToSave"></param>
    /// <param name="modelManager">Custom manager to use alongside the model</param>
    /// <param name="conn">Connection string to DB</param>
    public void BatchSave<T>(IEnumerable<T> dataToSave, IData modelManager, string conn)
    {
        var model = dataToSave.First();

        using (SqlConnection sqlconn= new SqlConnection(conn))
        {
            sqlconn.Open();

            using (SqlCommand cmd = new SqlCommand(GetCreateScript(modelManager, model), sqlconn))
            {
                //Create temp table to do initial insert into
                cmd.ExecuteNonQuery();

                SqlBulkCopy copy = new SqlBulkCopy(cmd.Connection);

                copy.DestinationTableName = "#tempTableForImport";

                // Convert data to DataTable
                DataTable dt = dataToSave.ToDataTable(modelManager.GetDataColumnsOrder());

                // Copy to temp table
                copy.WriteToServer(dt);
            }

            using (SqlCommand cmd = new SqlCommand(modelManager.GetInsertSproc(), sqlconn) { CommandType=CommandType.StoredProcedure })
            {
                // Clean up data and move to final table
                cmd.ExecuteNonQuery();
            }

            sqlconn.Close();
        }
    }

编辑#1:使用提出的建议新修改的代码,现在使用 Fastmember:

public void BatchSave<T>(IEnumerable<T> dataToSave, IData modelManager, string conn)
{
    var model = dataToSave.First();

    using (SqlConnection sqlconn = new SqlConnection(conn))
    {
        sqlconn.Open();

        using (var bcp = new SqlBulkCopy(sqlconn))
        {
            using (var reader = ObjectReader.Create(dataToSave, modelManager.GetDataColumnsOrder().Select(s => s.Item1).ToArray() /*modelManager.GetDataColumnsOrder().Select(obj=>obj.Item1).ToString()*/))
            {
                using (SqlCommand cmd = new SqlCommand(GetCreateScript(modelManager, model), sqlconn))
                {
                    cmd.ExecuteNonQuery();
                    bcp.DestinationTableName = "#tempTableForImport";
                    bcp.WriteToServer(reader);
                }

                using (SqlCommand cmd = new SqlCommand(modelManager.GetInsertSproc(), sqlconn) { CommandType = CommandType.StoredProcedure })
                {
                    cmd.ExecuteNonQuery();
                }
            }
        }

        sqlconn.Close();
    }
}

这加快了速度,但是我仍然在这条线上收到“超时已过期”信息

bcp.WriteToServer(reader);

感谢大家到目前为止的帮助,大约 30 秒后,对此还有更多想法吗?也许可以通过某种方式增加超时前的时间长度?

c# .net linq datatable
4个回答
5
投票

我不会通过 DataTable,而是为您的集合实现一个 IDataReader 并将其提供给 SqlBulkCopy。 如果正确完成并使用惰性 IEnumerable,它会比数据表路由更快,并且使用更少的内存。 Mark Gravell 已经编写了这样一个库,用于将 IEnumerables 转换为 IDataReader,我建议您在推出自己的库之前先检查一下。

FastMember 可以在 NuGet 上找到:https://www.nuget.org/packages/FastMember/ 原始来源在这里找到:https://code.google.com/p/fast-member/ 以及此线程中的示例:SqlBulkCopy from a List<>

更新: 您可能还需要更改命令超时,并在 sqlbulkcopy 上设置批处理大小,如下所示:

using (SqlCommand cmd = new SqlCommand(modelManager.GetInsertSproc(), sqlconn) { 
  CommandType = CommandType.StoredProcedure, CommandTimeout=300 })

bcp.BatchSize = 100000;

1
投票

不要转换。 DataTalble 超时并且占用大量内存。您可以使用TVP(表值参数)来非常快速地加载。 它就像一个反向数据读取器。 对于来自 IEnumable(不是 DataTable)的情况,请使用 SqlDataRecord。
只需一个链接 - 在 TVP SqlDataRecord 上搜索


1
投票

对于性能问题,很难提供具体的修复,但我会删除您实际上不需要的任何内容,从

BeginEdit()
EndEdit()
调用开始。您正在创建一个新行;除非您需要明确的行状态来处理您的问题中未描述的内容,否则这些会执行您可能不需要的额外操作。

可以尝试的另一件事是将集合拆分为块,然后使用

Parallel.For
/
Foreach
为每个块执行数据表创建,然后使用
DataTable.Merge()
将它们合并在一起并返回结果。


0
投票

基本上我有一个相当大的 IEnumerable (大约 16 - 17 百万 项目)到目前为止,我还没有真正遇到任何问题, 直到我尝试使用扩展方法转换为数据表:

根据文档,数据表中行的上限为

16,777,216

DataTable 可以存储的最大行数为 16,777,216。 有关更多信息,请参阅将数据添加到数据表。

© www.soinside.com 2019 - 2024. All rights reserved.