一种从IEnumerable <t>转换为DataTable的有效方法

时间:2015-10-02 16:40:52

标签: c# .net linq datatable

在此之前被标记为重复之前,我已经看到了许多像这样的答案 Convert IEnumerable to DataTable并尝试在创建扩展方法方面做类似的事情。我问我的问题,因为我可能会在其他地方撒谎。

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

/// <summary>
/// Converts IEnumberable 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转换为数据表? 我正在转换为DataTable,因此我可以使用SQLBulkCopy将数据导入数据库。

编辑0: 这是从

传递数据的地方
    /// <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();
    }
}

这已经加快了速度,但是我仍然在等待“超时”#39;在这一行bcp.WriteToServer(reader);。感谢所有人在大约30秒之后的帮助,对此有任何想法吗?或许可以在超时之前增加一段时间?

4 个答案:

答案 0 :(得分:4)

我不会通过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 :(得分:1)

由于性能问题很难提供特定的修复,但我会删除你不需要的任何东西,从BeginEdit()和EndEdit()调用开始。您正在创建一个新行,除非您需要一个明确的rowstate用于您的问题中未描述的内容,然后这些正在做您可能不需要的额外内容。

可以尝试的另一件事是将您的集合拆分为块,然后使用Parallel.For / Foreach为每个块执行数据表创建,然后使用DataTable.Merge()将它们合并回来并返回结果

答案 2 :(得分:1)

不要转换。 DataTalble超时并且是内存耗尽。您可以使用TVP(表值参数)加载非常快。它就像一个反向数据阅读器。来自IEnumable(不是DataTable)使用SqlDataRecord just one link - search on TVP SqlDataRecord

答案 3 :(得分:0)

  

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

根据documentation,DataTable中行的上限为16,777,216

  

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