SqlBulkCopy 单事务下多表插入或实体框架与经典 Ado.net 之间的批量插入操作

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

我有两个表需要在我的应用程序运行时插入。
假设我有如下表格

  • tbl_FirstTable 和 tbl_SecondTable

我的问题是数据量。
我需要向 tbl_FirstTable 插入超过 10,000 行,向 tbl_SecondTable 插入超过 500,000 行。

首先,我使用实体框架如下。

public bool Save_tbl_FirstTable_Vs_tbl_SecondTable(List<tbl_FirstTable> List_tbl_FirstTable, List<tbl_SecondTable> List_tbl_SecondTable)
{
    bool IsSuccessSave = false;
    try
    {
        using (DummyDBClass_ObjectContext _DummyDBClass_ObjectContext = new DummyDBClass_ObjectContext())
        {           
            foreach (tbl_FirstTable _tbl_FirstTable in List_tbl_FirstTable)
            {
                _DummyDBClass_ObjectContext.tbl_FirstTable.InsertOnSubmit(_tbl_FirstTable);
            }

            foreach (tbl_SecondTable _tbl_SecondTable in List_tbl_SecondTable)
            {
                _DummyDBClass_ObjectContext.tbl_SecondTable.InsertOnSubmit(_tbl_SecondTable);
            }

            _DummyDBClass_ObjectContext.SubmitChanges();
            IsSuccessSave = true;
        }
    }
    catch (Exception ex)
    {
        Log4NetWrapper.WriteError(string.Format("{0} : {1} : Exception={2}",
                                    this.GetType().FullName,
                                    (new StackTrace(new StackFrame(0))).GetFrame(0).GetMethod().Name.ToString(),
                                    ex.Message.ToString()));

        if (ex.InnerException != null)
        {
            Log4NetWrapper.WriteError(string.Format("{0} : {1} : InnerException Exception={2}",
                                    this.GetType().FullName,
                                    (new StackTrace(new StackFrame(0))).GetFrame(0).GetMethod().Name.ToString(),
                                    ex.InnerException.Message.ToString()));
        }
    }

    return IsSuccessSave;
}

这就是我面临错误的地方

Time out exception

我认为如果我使用下面的代码,该异常将会得到解决。

DummyDBClass_ObjectContext.CommandTimeout = 1800; // 30 minutes

所以我就用了它。它解决了,但我面临另一个错误

OutOfMemory Exception

于是我就去寻找解决方案,幸运的是,我找到了下面的文章。

  1. 使用实体框架批量插入时出现问题
  2. 通过 SqlBulkCopy 使用事务
  3. 在事务中执行批量复制操作

根据该文章,我将代码从实体框架更改为经典 ADO.net 代码。

public bool Save_tbl_FirstTable_Vs_tbl_SecondTable(DataTable DT_tbl_FirstTable, DataTable DT_tbl_SecondTable)
{
    bool IsSuccessSave = false;
    SqlTransaction transaction = null;
    try
    {
        using (DummyDBClass_ObjectContext _DummyDBClass_ObjectContext = new DummyDBClass_ObjectContext())
        {
            var connectionString = ((EntityConnection)_DummyDBClass_ObjectContext.Connection).StoreConnection.ConnectionString;
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (transaction = connection.BeginTransaction())
                {
                    using (SqlBulkCopy bulkCopy_tbl_FirstTable = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))                            
                    {
                        bulkCopy_tbl_FirstTable.BatchSize = 5000;
                        bulkCopy_tbl_FirstTable.DestinationTableName = "dbo.tbl_FirstTable";
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("ID", "ID");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("UploadFileID", "UploadFileID");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("Active", "Active");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("CreatedUserID", "CreatedUserID");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("CreatedDate", "CreatedDate");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("UpdatedUserID", "UpdatedUserID");
                        bulkCopy_tbl_FirstTable.ColumnMappings.Add("UpdatedDate", "UpdatedDate");
                        bulkCopy_tbl_FirstTable.WriteToServer(DT_tbl_FirstTable);
                    }

                    using (SqlBulkCopy bulkCopy_tbl_SecondTable = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))                            
                    {

                        bulkCopy_tbl_SecondTable.BatchSize = 5000;
                        bulkCopy_tbl_SecondTable.DestinationTableName = "dbo.tbl_SecondTable";
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("ID", "ID");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("UploadFileDetailID", "UploadFileDetailID");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("CompaignFieldMasterID", "CompaignFieldMasterID");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("Value", "Value");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("Active", "Active");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("CreatedUserID", "CreatedUserID");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("CreatedDate", "CreatedDate");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("UpdatedUserID", "UpdatedUserID");
                        bulkCopy_tbl_SecondTable.ColumnMappings.Add("UpdatedDate", "UpdatedDate");
                        bulkCopy_tbl_SecondTable.WriteToServer(DT_tbl_SecondTable);
                    }


                    transaction.Commit();
                    IsSuccessSave = true;
                }
                connection.Close();
            }
        }
    }
    catch (Exception ex)
    {
        if (transaction != null)
            transaction.Rollback();

        Log4NetWrapper.WriteError(string.Format("{0} : {1} : Exception={2}",
                                    this.GetType().FullName,
                                    (new StackTrace(new StackFrame(0))).GetFrame(0).GetMethod().Name.ToString(),
                                    ex.Message.ToString()));

        if (ex.InnerException != null)
        {
            Log4NetWrapper.WriteError(string.Format("{0} : {1} : InnerException Exception={2}",
                                    this.GetType().FullName,
                                    (new StackTrace(new StackFrame(0))).GetFrame(0).GetMethod().Name.ToString(),
                                    ex.InnerException.Message.ToString()));
        }
    }

    return IsSuccessSave;
}

最后,它在不到 15 秒的时间内执行超过 500,000 行的插入过程。

我发布这个场景有两个原因。

  1. 我想分享我的发现。
  2. 由于我并不完美,我还需要得到你们更多的建议。

因此,每一个更好的解决方案都将受到赞赏。

c# sql-server entity-framework ado.net
4个回答
2
投票

1)使用EF6.x,它的性能比EF5.x好很多

这里有更多建议(来自使用 EF 批量插入

2)通过为每个工作单元使用新的上下文来保持活动上下文图较小

3) 关闭 AutoDetechChangesEnabled - context.Configuration.AutoDetectChangesEnabled = false;

4)批处理,在循环中定期调用 SaveChanges


1
投票

我使用来自 ZZZ Projects 的付费实体框架扩展,由于流畅的 API(扩展方法、功能方法),它对开发人员很友好。这不是广告,我在商业项目中使用它好几年了,它很棒。如果您想免费使用某些东西并且您拥有 Oracle 数据库,则 Oracle Managed Data Access Oracle.ManagedDataAccess.Core 可以实现批量操作。


1
投票

批量操作并不是 ORM 的真正用途。对于批量插入操作,我将 xml 发送到存储过程,然后将其分解并从那里批量插入/更新或合并。 因此,即使当我使用 ORM 时,我也会创建一个不依赖 EF(或 NHibernate)的域库。因此,在某些情况下我有一个“安全阀”来绕过 ORM。

您应该考虑使用 System.Data.SqlClient.SqlBulkCopy 来实现此目的。这是文档 - http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlbulkcopy.aspx,当然还有大量在线教程。

如果我们希望 EF 批量插入记录,建议使用以下几点来提高性能

  • 例如在 100 条记录后调用 SaveChanges() 并释放上下文并创建一个新记录。
  • 禁用变更检测

示例:

using (TransactionScope scope = new TransactionScope())
{
    MyDbContext context = null;
    try
    {
        context = new MyDbContext();
        context.Configuration.AutoDetectChangesEnabled = false;

        int count = 0;            
        foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
        {
            ++count;
            context = AddToContext(context, entityToInsert, count, 100, true);
        }

        context.SaveChanges();
    }
    finally
    {
        if (context != null)
            context.Dispose();
    }

    scope.Complete();
}

private MyDbContext AddToContext(MyDbContext context,
    Entity entity, int count, int commitCount, bool recreateContext)
{
    context.Set<Entity>().Add(entity);

    if (count % commitCount == 0)
    {
        context.SaveChanges();
        if (recreateContext)
        {
            context.Dispose();
            context = new MyDbContext();
            context.Configuration.AutoDetectChangesEnabled = false;
        }
    }

    return context;
}

对于性能来说,在“许多”记录(“许多”大约 100 或 1000 条)之后调用 SaveChanges() 非常重要。它还提高了在 SaveChanges 后处理上下文并创建新上下文的性能。

这会清除所有实体的上下文,SaveChanges 不会执行此操作,实体仍以“未更改”状态附加到上下文。正是上下文中附加实体的大小不断增长,逐步减慢了插入速度。 因此,在一段时间后清除它会很有帮助。

AutoDetectChangesEnabled = false;在 DbContext 上。

它还具有很大的额外性能影响:为什么与 ObjectContext 相比,在 EF 4.1 中插入实体如此慢?.

下面的组合可以很好地提高 EF 的速度。

  • context.Configuration.AutoDetectChangesEnabled = false;
  • context.Configuration.ValidateOnSaveEnabled = false;

0
投票

我最终写了一些东西来直接解决这个问题,它可以作为 NuGet 包的一部分提供:https://www.nuget.org/packages/EntityFrameworkCore.Toolbox

有几个选项,要么自己构造实体,要么提供属性映射以从另一个输入对象获取正确的值:

await dbContext.BulkCopyAsync(myDataEntities);

await dbContext.BulkCopyAsync<TEntity, TSource>>(myDataSourceObjects, entityNameToPropertyGetterMap);
© www.soinside.com 2019 - 2024. All rights reserved.