簡體   English   中英

在實體框架中插入的最快方法

[英]Fastest Way of Inserting in Entity Framework

我正在尋找插入實體框架的最快方法。

我問這個是因為您有一個活動的TransactionScope並且插入量很大(4000+)。 它可能會持續超過 10 分鍾(交易的默認超時),這將導致交易不完整。

對於您在問題評論中的評論:

“...SavingChanges(對於每條記錄)...”

這是你能做的最糟糕的事情! 為每條記錄調用SaveChanges()會極大地減慢批量插入的速度。 我會做一些簡單的測試,這很可能會提高性能:

  • 在所有記錄后調用SaveChanges()一次。
  • 例如,在 100 條記錄后調用SaveChanges()
  • 在例如 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;
}

我有一個測試程序,它將 560.000 個實體(9 個標量屬性,沒有導航屬性)插入到數據庫中。 使用此代碼,它可以在不到 3 分鍾的時間內工作。

為了提高性能,在“許多”記錄(“許多”大約 100 或 1000)之后調用SaveChanges()很重要。 它還提高了在 SaveChanges 之后處理上下文並創建新上下文的性能。 這會清除所有實體的上下文, SaveChanges不會這樣做,實體仍以Unchanged狀態附加到上下文。 上下文中附加實體的大小不斷增加,從而逐步減慢了插入速度。 因此,一段時間后清除它會很有幫助。

以下是我的 560000 個實體的一些測量結果:

  • commitCount = 1, recreateContext = false: many hours (那是你目前的程序)
  • commitCount = 100,recreateContext = false:超過20分鍾
  • commitCount = 1000,recreateContext = false: 242 秒
  • commitCount = 10000,recreateContext = false: 202 秒
  • commitCount = 100000,recreateContext = false: 199 秒
  • commitCount = 1000000, recreateContext = false:內存不足異常
  • commitCount = 1,recreateContext = true:超過10分鍾
  • commitCount = 10,recreateContext = true: 241 秒
  • commitCount = 100,recreateContext = true: 164 秒
  • commitCount = 1000,recreateContext = true: 191 秒

上面第一個測試中的行為是性能非常非線性並且隨着時間的推移急劇下降。 (“許多小時”是一個估計值,我從未完成此測試,20 分鍾后我在 50.000 個實體處停止。)這種非線性行為在所有其他測試中並不那么重要。

這種組合可以很好地提高速度。

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

最快的方法是使用我開發的批量插入擴展

注意:這是一個商業產品,不是免費的

它使用 SqlBulkCopy 和自定義數據讀取器來獲得最大性能。 因此,它比使用常規插入或 AddRange 快 20 倍以上EntityFramework.BulkInsert 與 EF AddRange

使用極其簡單

context.BulkInsert(hugeAmountOfEntities);

為此,您應該考慮使用System.Data.SqlClient.SqlBulkCopy 這是文檔,當然還有很多在線教程。

抱歉,我知道您正在尋找一個簡單的答案來讓 EF 做您想做的事情,但是批量操作並不是 ORM 真正的用途。

我同意亞當·拉基斯的觀點。 SqlBulkCopy是將批量記錄從一個數據源傳輸到另一個數據源的最快方法。 我用它復制了 20K 條記錄,耗時不到 3 秒。 看看下面的例子。

public static void InsertIntoMembers(DataTable dataTable)
{           
    using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
    {
        SqlTransaction transaction = null;
        connection.Open();
        try
        {
            transaction = connection.BeginTransaction();
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = "Members";
                sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
                sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
                sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
                sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
                sqlBulkCopy.ColumnMappings.Add("Email", "Email");

                sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
                sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
                sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
                sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
                sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");

                sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
                sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");

                sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
        }

    }
}

因為這里從未提到過我想在這里推薦 EFCore.BulkExtensions

context.BulkInsert(entitiesList);                 context.BulkInsertAsync(entitiesList);
context.BulkUpdate(entitiesList);                 context.BulkUpdateAsync(entitiesList);
context.BulkDelete(entitiesList);                 context.BulkDeleteAsync(entitiesList);
context.BulkInsertOrUpdate(entitiesList);         context.BulkInsertOrUpdateAsync(entitiesList);         // Upsert
context.BulkInsertOrUpdateOrDelete(entitiesList); context.BulkInsertOrUpdateOrDeleteAsync(entitiesList); // Sync
context.BulkRead(entitiesList);                   context.BulkReadAsync(entitiesList);

我會推薦這篇關於如何使用 EF 進行批量插入的文章。

實體框架和慢速批量插入

他探索了這些領域並比較了性能:

  1. 默認 EF(57 分鍾完成添加 30,000 條記錄)
  2. 替換為 ADO.NET 代碼(相同的 30,000 需要 25
  3. 上下文膨脹 - 通過為每個工作單元使用新上下文來保持活動上下文圖較小(相同的 30,000 次插入需要 33 秒)
  4. 大列表 - 關閉 AutoDetectChangesEnabled(將時間縮短到大約 20 秒)
  5. 批處理(低至 16 秒)
  6. DbTable.AddRange() - (性能在 12 范圍內)

我已經調查了 Slauma 的答案(這很棒,感謝您的想法),並且我已經減少了批量大小,直到達到最佳速度。 查看 Slauma 的結果:

  • commitCount = 1,recreateContext = true:超過10分鍾
  • commitCount = 10,recreateContext = true:241 秒
  • commitCount = 100,recreateContext = true:164 秒
  • commitCount = 1000,recreateContext = true:191 秒

可見從1移動到10,從10到100有速度提升,但是從100到1000插入速度又下降了。

所以我專注於當你將批量大小減少到 10 到 100 之間的某個值時會發生什么,這是我的結果(我使用不同的行內容,所以我的時間有不同的價值):

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

根據我的結果,批量大小的實際最佳值約為 30。 它小於 10 和 100。問題是,我不知道為什么 30 是最佳的,我也找不到任何合乎邏輯的解釋。

正如其他人所說,如果您想要真正良好的插入性能,那么 SqlBulkCopy 就是這樣做的方法。

實現起來有點麻煩,但有一些庫可以幫助你。 有一些,但這次我會無恥地插入我自己的庫: https : //github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

您需要的唯一代碼是:

 using (var db = new YourDbContext())
 {
     EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
 }

那么它的速度有多快? 很難說,因為它取決於很多因素,計算機性能、網絡、對象大小等。我所做的性能測試表明,如果您優化了 EF 配置,則可以在 10 秒左右以標准方式在本地主機上插入 25k 個實體在其他答案中提到。 使用 EFUtilities 大約需要 300 毫秒。 更有趣的是,我使用這種方法在 15 秒內保存了大約 300 萬個實體,平均每秒大約 20 萬個實體。

一個問題當然是如果您需要插入相關數據。 這可以使用上面的方法在 sql server 中有效地完成,但它需要您有一個 Id 生成策略,讓您在應用程序代碼中為父級生成 id,以便您可以設置外鍵。 這可以使用 GUID 或 HiLo id 生成之類的東西來完成。

如果您Add()的實體依賴於上下文中的其他預加載實體(例如導航屬性),則Dispose()上下文會產生問題

我使用類似的概念來保​​持我的上下文較小以實現相同的性能

但不是Dispose()上下文並重新創建,我只是分離已經SaveChanges()的實體

public void AddAndSave<TEntity>(List<TEntity> entities) where TEntity : class {

const int CommitCount = 1000; //set your own best performance number here
int currentCount = 0;

while (currentCount < entities.Count())
{
    //make sure it don't commit more than the entities you have
    int commitCount = CommitCount;
    if ((entities.Count - currentCount) < commitCount)
        commitCount = entities.Count - currentCount;

    //e.g. Add entities [ i = 0 to 999, 1000 to 1999, ... , n to n+999... ] to conext
    for (int i = currentCount; i < (currentCount + commitCount); i++)        
        _context.Entry(entities[i]).State = System.Data.EntityState.Added;
        //same as calling _context.Set<TEntity>().Add(entities[i]);       

    //commit entities[n to n+999] to database
    _context.SaveChanges();

    //detach all entities in the context that committed to database
    //so it won't overload the context
    for (int i = currentCount; i < (currentCount + commitCount); i++)
        _context.Entry(entities[i]).State = System.Data.EntityState.Detached;

    currentCount += commitCount;
} }

如果需要,用 try catch 和TrasactionScope()包裝它,不要在此處顯示它們以保持代碼清潔

我知道這是一個非常古老的問題,但是這里的一個人說開發了一種擴展方法來使用 EF 批量插入,當我檢查時,我發現今天該庫的成本為 599 美元(對於一個開發人員)。 也許這對整個庫都有意義,但是對於批量插入來說這太多了。

這是我制作的一個非常簡單的擴展方法。 我首先將它與數據庫配對使用(先不使用代碼進行測試,但我認為它的工作原理相同)。 使用您的上下文名稱更改YourEntities

public partial class YourEntities : DbContext
{
    public async Task BulkInsertAllAsync<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            await conn.OpenAsync();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            await bulkCopy.WriteToServerAsync(table);
        }
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            bulkCopy.WriteToServer(table);
        }
    }

    public string GetTableName(Type type)
    {
        var metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                .Single(e => objectItemCollection.GetClrType(e) == type);

        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
            .Single()
            .EntitySets
            .Single(s => s.ElementType.Name == entityType.Name);

        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                .Single()
                .EntitySetMappings
                .Single(s => s.EntitySet == entitySet);

        var table = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .StoreEntitySet;

        return (string)table.MetadataProperties["Table"].Value ?? table.Name;
    }
}

您可以對繼承自IEnumerable任何集合使用它,如下所示:

await context.BulkInsertAllAsync(items);

[2019 更新] EF Core 3.1

按照上面所說的,在 EF Core 中禁用 AutoDetectChangesEnabled 效果很好:插入時間除以 100(從幾分鍾到幾秒,具有交叉表關系的 10k 記錄)

更新后的代碼是:

context.ChangeTracker.AutoDetectChangesEnabled = false;
foreach (IRecord record in records) {
    //Add records to your database        
}
context.ChangeTracker.DetectChanges();
context.SaveChanges();
context.ChangeTracker.AutoDetectChangesEnabled = true; //do not forget to re-enable

我對上面的@Slauma 的例子做了一個通用的擴展;

public static class DataExtensions
{
    public static DbContext AddToContext<T>(this DbContext context, object entity, int count, int commitCount, bool recreateContext, Func<DbContext> contextCreator)
    {
        context.Set(typeof(T)).Add((T)entity);

        if (count % commitCount == 0)
        {
            context.SaveChanges();
            if (recreateContext)
            {
                context.Dispose();
                context = contextCreator.Invoke();
                context.Configuration.AutoDetectChangesEnabled = false;
            }
        }
        return context;
    }
}

用法:

public void AddEntities(List<YourEntity> entities)
{
    using (var transactionScope = new TransactionScope())
    {
        DbContext context = new YourContext();
        int count = 0;
        foreach (var entity in entities)
        {
            ++count;
            context = context.AddToContext<TenancyNote>(entity, count, 100, true,
                () => new YourContext());
        }
        context.SaveChanges();
        transactionScope.Complete();
    }
}

保存列表的最快方法之一,您必須應用以下代碼

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

AutoDetectChangesEnabled = false

添加、添加范圍和保存更改:不檢測更改。

ValidateOnSaveEnabled = false;

不檢測更改跟蹤器

你必須添加nuget

Install-Package Z.EntityFramework.Extensions

現在您可以使用以下代碼

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();

嘗試使用存儲過程來獲取要插入的數據的 XML。

我正在尋找插入實體框架的最快方法

有一些支持批量插入的第三方庫可用:

  • Z.EntityFramework.Extensions(推薦
  • EF公用事業
  • EntityFramework.BulkInsert

請參閱:實體框架批量插入庫

選擇批量插入庫時要小心。 只有實體框架擴展支持所有類型的關聯和繼承,並且它是唯一仍然受支持的。


免責聲明:我是實體框架擴展的所有者

該庫允許您執行場景所需的所有批量操作:

  • 批量保存更改
  • 批量插入
  • 批量刪除
  • 批量更新
  • 批量合並

例子

// Easy to use
context.BulkSaveChanges();

// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);

// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);

// Customize Primary Key
context.BulkMerge(customers, operation => {
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
});

SqlBulkCopy 超級快

這是我的實現:

// at some point in my calling code, I will call:
var myDataTable = CreateMyDataTable();
myDataTable.Rows.Add(Guid.NewGuid,tableHeaderId,theName,theValue); // e.g. - need this call for each row to insert

var efConnectionString = ConfigurationManager.ConnectionStrings["MyWebConfigEfConnection"].ConnectionString;
var efConnectionStringBuilder = new EntityConnectionStringBuilder(efConnectionString);
var connectionString = efConnectionStringBuilder.ProviderConnectionString;
BulkInsert(connectionString, myDataTable);

private DataTable CreateMyDataTable()
{
    var myDataTable = new DataTable { TableName = "MyTable"};
// this table has an identity column - don't need to specify that
    myDataTable.Columns.Add("MyTableRecordGuid", typeof(Guid));
    myDataTable.Columns.Add("MyTableHeaderId", typeof(int));
    myDataTable.Columns.Add("ColumnName", typeof(string));
    myDataTable.Columns.Add("ColumnValue", typeof(string));
    return myDataTable;
}

private void BulkInsert(string connectionString, DataTable dataTable)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlTransaction transaction = null;
        try
        {
            transaction = connection.BeginTransaction();

            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns) {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction?.Rollback();
            throw;
        }
    }
}

使用SqlBulkCopy

void BulkInsert(GpsReceiverTrack[] gpsReceiverTracks)
{
    if (gpsReceiverTracks == null)
    {
        throw new ArgumentNullException(nameof(gpsReceiverTracks));
    }

    DataTable dataTable = new DataTable("GpsReceiverTracks");
    dataTable.Columns.Add("ID", typeof(int));
    dataTable.Columns.Add("DownloadedTrackID", typeof(int));
    dataTable.Columns.Add("Time", typeof(TimeSpan));
    dataTable.Columns.Add("Latitude", typeof(double));
    dataTable.Columns.Add("Longitude", typeof(double));
    dataTable.Columns.Add("Altitude", typeof(double));

    for (int i = 0; i < gpsReceiverTracks.Length; i++)
    {
        dataTable.Rows.Add
        (
            new object[]
            {
                    gpsReceiverTracks[i].ID,
                    gpsReceiverTracks[i].DownloadedTrackID,
                    gpsReceiverTracks[i].Time,
                    gpsReceiverTracks[i].Latitude,
                    gpsReceiverTracks[i].Longitude,
                    gpsReceiverTracks[i].Altitude
            }
        );
    }

    string connectionString = (new TeamTrackerEntities()).Database.Connection.ConnectionString;
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns)
                {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
    }

    return;
}

以下是在實際示例中使用實體框架和使用 SqlBulkCopy 類之間的性能比較:如何將復雜對象批量插入 SQL Server 數據庫

正如其他人已經強調的那樣,ORM 不打算用於批量操作。 它們提供了靈活性、關注點分離和其他好處,但批量操作(批量讀取除外)不是其中之一。

另一種選擇是使用 Nuget 提供的 SqlBulkTools。 它非常易於使用並具有一些強大的功能。

例子:

var bulk = new BulkOperations();
var books = GetBooks();

using (TransactionScope trans = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(ConfigurationManager
    .ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
    {
        bulk.Setup<Book>()
            .ForCollection(books)
            .WithTable("Books") 
            .AddAllColumns()
            .BulkInsert()
            .Commit(conn);
    }

    trans.Complete();
}

有關更多示例和高級用法,請參閱文檔 免責聲明:我是這個圖書館的作者,任何觀點都是我自己的觀點。

[POSTGRESQL 的新解決方案] 嘿,我知道這是一篇很老的帖子,但我最近遇到了類似的問題,但我們使用的是 Postgresql。 我想使用有效的bulkinsert,結果非常困難。 我還沒有找到任何合適的免費庫來在這個數據庫上這樣做。 我只找到了這個助手: https ://bytefish.de/blog/postgresql_bulk_insert/,它也在 Nuget 上。 我編寫了一個小型映射器,它以實體框架的方式自動映射屬性:

public static PostgreSQLCopyHelper<T> CreateHelper<T>(string schemaName, string tableName)
        {
            var helper = new PostgreSQLCopyHelper<T>("dbo", "\"" + tableName + "\"");
            var properties = typeof(T).GetProperties();
            foreach(var prop in properties)
            {
                var type = prop.PropertyType;
                if (Attribute.IsDefined(prop, typeof(KeyAttribute)) || Attribute.IsDefined(prop, typeof(ForeignKeyAttribute)))
                    continue;
                switch (type)
                {
                    case Type intType when intType == typeof(int) || intType == typeof(int?):
                        {
                            helper = helper.MapInteger("\"" + prop.Name + "\"",  x => (int?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type stringType when stringType == typeof(string):
                        {
                            helper = helper.MapText("\"" + prop.Name + "\"", x => (string)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type dateType when dateType == typeof(DateTime) || dateType == typeof(DateTime?):
                        {
                            helper = helper.MapTimeStamp("\"" + prop.Name + "\"", x => (DateTime?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type decimalType when decimalType == typeof(decimal) || decimalType == typeof(decimal?):
                        {
                            helper = helper.MapMoney("\"" + prop.Name + "\"", x => (decimal?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type doubleType when doubleType == typeof(double) || doubleType == typeof(double?):
                        {
                            helper = helper.MapDouble("\"" + prop.Name + "\"", x => (double?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type floatType when floatType == typeof(float) || floatType == typeof(float?):
                        {
                            helper = helper.MapReal("\"" + prop.Name + "\"", x => (float?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type guidType when guidType == typeof(Guid):
                        {
                            helper = helper.MapUUID("\"" + prop.Name + "\"", x => (Guid)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                }
            }
            return helper;
        }

我按以下方式使用它(我有一個名為 Undertaking 的實體):

var undertakingHelper = BulkMapper.CreateHelper<Model.Undertaking>("dbo", nameof(Model.Undertaking));
undertakingHelper.SaveAll(transaction.UnderlyingTransaction.Connection as Npgsql.NpgsqlConnection, undertakingsToAdd));

我展示了一個事務示例,但也可以使用從上下文中檢索到的正常連接來完成。 workingsToAdd 是可枚舉的普通實體記錄,我想將其批量插入到數據庫中。

經過幾個小時的研究和嘗試,我得到了這個解決方案,正如您所期望的那樣,速度更快,最終易於使用且免費! 我真的建議你使用這個解決方案,不僅是因為上面提到的原因,而且因為它是唯一一個我對 Postgresql 本身沒有問題的解決方案,許多其他解決方案可以完美地工作,例如與 SqlServer。

是的, SqlBulkUpdate確實是處理此類任務的最快工具。 我想在 .NET Core 中為我找到“最省力”的通用方式,所以我最終使用了來自 Marc Gravell 的名為 FastMember 的優秀庫,並為實體框架 DB 上下文編寫了一個微小的擴展方法。 工作閃電般快速:

using System.Collections.Generic;
using System.Linq;
using FastMember;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

namespace Services.Extensions
{
    public static class DbContextExtensions
    {
        public static void BulkCopyToServer<T>(this DbContext db, IEnumerable<T> collection)
        {
            var messageEntityType = db.Model.FindEntityType(typeof(T));

            var tableName = messageEntityType.GetSchema() + "." + messageEntityType.GetTableName();
            var tableColumnMappings = messageEntityType.GetProperties()
                .ToDictionary(p => p.PropertyInfo.Name, p => p.GetColumnName());

            using (var connection = new SqlConnection(db.Database.GetDbConnection().ConnectionString))
            using (var bulkCopy = new SqlBulkCopy(connection))
            {
                foreach (var (field, column) in tableColumnMappings)
                {
                    bulkCopy.ColumnMappings.Add(field, column);
                }

                using (var reader = ObjectReader.Create(collection, tableColumnMappings.Keys.ToArray()))
                {
                    bulkCopy.DestinationTableName = tableName;
                    connection.Open();
                    bulkCopy.WriteToServer(reader);
                    connection.Close();
                }
            }
        }
    }
}

你有沒有試過通過后台工作人員或任務插入?

就我而言,我插入了 7760 個寄存器,分布在 182 個具有外鍵關系的不同表中(通過 NavigationProperties)。

沒有任務,花了2分半鍾。 在一個任務( Task.Factory.StartNew(...) )中,花費了 15 秒。

我只在將所有實體添加到上下文后才執行SaveChanges() (確保數據完整性)

這里寫的所有解決方案都無濟於事,因為當您執行 SaveChanges() 時,插入語句被一條一條地發送到數據庫,這就是 Entity 的工作原理。

例如,如果您到數據庫和返回的行程是 50 毫秒,那么插入所需的時間是記錄數 x 50 毫秒。

您必須使用 BulkInsert,這是鏈接: https ://efbulkinsert.codeplex.com/

通過使用它,我將插入時間從 5-6 分鍾減少到 10-12 秒。

據我所知, EntityFramework no BulkInsert來提高巨大插入的性能。

在這種情況下,您可以去使用SqlBulkCopyADO.net為您解決問題

您可以使用批量包庫。 批量插入 1.0.0 版本用於具有實體框架 >=6.0.0 的項目。

更多描述可以在這里找到-批量操作源代碼

秘訣是插入到相同的空白臨時表中。 插入物正在快速減輕。 然后從中運行單個插入到您的主大表中。 然后截斷為下一批准備的臨時表。

IE。

insert into some_staging_table using Entity Framework.

-- Single insert into main table (this could be a tiny stored proc call)
insert into some_main_already_large_table (columns...)
   select (columns...) from some_staging_table
truncate table some_staging_table

TL; DR我知道這是一篇舊帖子,但我已經實現了一個解決方案,從一個提出的解決方案開始,通過擴展它並解決了一些問題; 此外,我還閱讀了提出的其他解決方案,與這些解決方案相比,在我看來,提出了一個更適合原始問題中提出的要求的解決方案。

在這個解決方案中,我擴展了Slauma 的方法,我認為它非常適合原始問題中提出的案例,即使用實體框架和事務范圍對數據庫進行昂貴的寫操作。

在 Slauma 的解決方案中 - 順便說一句,這是一個草案,僅用於通過實施批量插入的策略來了解 EF 的速度 - 存在以下問題:

  1. 交易的超時時間(默認為 1 分鍾,可通過代碼延長至最多 10 分鍾);
  2. 第一個數據塊的重復,其寬度等於事務結束時使用的提交的大小(這個問題很奇怪,可以通過變通方法規避)。

我還通過報告一個示例擴展了 Slauma 提供的案例研究,該示例包括幾個依賴實體的上下文插入。

我已經能夠驗證的性能是 10K rec/min 在 db 中插入一個 200K 寬的記錄塊,每個記錄塊大約為 1KB。 速度恆定,性能沒有下降,測試大約需要 20 分鍾才能成功運行。

詳細的解決方案

主持插入到示例存儲庫類中的批量插入操作的方法:

abstract class SomeRepository { 

    protected MyDbContext myDbContextRef;

    public void ImportData<TChild, TFather>(List<TChild> entities, TFather entityFather)
            where TChild : class, IEntityChild
            where TFather : class, IEntityFather
    {

        using (var scope = MyDbContext.CreateTransactionScope())
        {

            MyDbContext context = null;
            try
            {
                context = new MyDbContext(myDbContextRef.ConnectionString);

                context.Configuration.AutoDetectChangesEnabled = false;

                entityFather.BulkInsertResult = false;
                var fileEntity = context.Set<TFather>().Add(entityFather);
                context.SaveChanges();

                int count = 0;

                //avoids an issue with recreating context: EF duplicates the first commit block of data at the end of transaction!!
                context = MyDbContext.AddToContext<TChild>(context, null, 0, 1, true);

                foreach (var entityToInsert in entities)
                {
                    ++count;
                    entityToInsert.EntityFatherRefId = fileEntity.Id;
                    context = MyDbContext.AddToContext<TChild>(context, entityToInsert, count, 100, true);
                }

                entityFather.BulkInsertResult = true;
                context.Set<TFather>().Add(fileEntity);
                context.Entry<TFather>(fileEntity).State = EntityState.Modified;

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

            scope.Complete();
        }

    }

}

僅用於示例目的的接口:

public interface IEntityChild {

    //some properties ...

    int EntityFatherRefId { get; set; }

}

public interface IEntityFather {

    int Id { get; set; }
    bool BulkInsertResult { get; set; }
}

我將解決方案的各種元素實現為靜態方法的 db 上下文:

public class MyDbContext : DbContext
{

    public string ConnectionString { get; set; }


    public MyDbContext(string nameOrConnectionString)
    : base(nameOrConnectionString)
    {
        Database.SetInitializer<MyDbContext>(null);
        ConnectionString = Database.Connection.ConnectionString;
    }


    /// <summary>
    /// Creates a TransactionScope raising timeout transaction to 30 minutes
    /// </summary>
    /// <param name="_isolationLevel"></param>
    /// <param name="timeout"></param>
    /// <remarks>
    /// It is possible to set isolation-level and timeout to different values. Pay close attention managing these 2 transactions working parameters.
    /// <para>Default TransactionScope values for isolation-level and timeout are the following:</para>
    /// <para>Default isolation-level is "Serializable"</para>
    /// <para>Default timeout ranges between 1 minute (default value if not specified a timeout) to max 10 minute (if not changed by code or updating max-timeout machine.config value)</para>
    /// </remarks>
    public static TransactionScope CreateTransactionScope(IsolationLevel _isolationLevel = IsolationLevel.Serializable, TimeSpan? timeout = null)
    {
        SetTransactionManagerField("_cachedMaxTimeout", true);
        SetTransactionManagerField("_maximumTimeout", timeout ?? TimeSpan.FromMinutes(30));

        var transactionOptions = new TransactionOptions();
        transactionOptions.IsolationLevel = _isolationLevel;
        transactionOptions.Timeout = TransactionManager.MaximumTimeout;
        return new TransactionScope(TransactionScopeOption.Required, transactionOptions);
    }

    private static void SetTransactionManagerField(string fieldName, object value)
    {
        typeof(TransactionManager).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, value);
    }


    /// <summary>
    /// Adds a generic entity to a given context allowing commit on large block of data and improving performance to support db bulk-insert operations based on Entity Framework
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context"></param>
    /// <param name="entity"></param>
    /// <param name="count"></param>
    /// <param name="commitCount">defines the block of data size</param>
    /// <param name="recreateContext"></param>
    /// <returns></returns>
    public static MyDbContext AddToContext<T>(MyDbContext context, T entity, int count, int commitCount, bool recreateContext) where T : class
    {
        if (entity != null)
            context.Set<T>().Add(entity);

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

        return context;
    }
}

做幾個筆記、評論和答案,這是我的實現,從我的實體獲取 SQL 連接字符串(我只在某些部分使用 SQLBulk,其余的實體框架)並使用與 SQL 數據庫相同的 Datetable 列名稱。

   public  void InsertBulkDatatable(DataTable dataTable)
        {
            EntityConnectionStringBuilder entityBuilder =  new EntityConnectionStringBuilder(ConfigurationManager.ConnectionStrings["MyDbContextConnectionName"].ConnectionString);
            string cs = entityBuilder.ProviderConnectionString;
            using (var connection = new SqlConnection(cs))
            {
                SqlTransaction transaction = null;
                connection.Open();
                try
                {
                    transaction = connection.BeginTransaction();
                    using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
                    {
                        sqlBulkCopy.DestinationTableName = dataTable.TableName; //Uses the SQL datatable to name the datatable in c#
                        //Maping Columns
                        foreach (DataColumn column in dataTable.Columns) {
                            sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);

                        }


                        sqlBulkCopy.WriteToServer(dataTable);
                    }
                    transaction.Commit();
                }
                catch (Exception)
                {
                    transaction.Rollback();
                }

            }
        }

使用以XML形式獲取輸入數據的存儲過程來插入數據。

從您的C#代碼中將插入數據作為xml傳遞。

例如在C#中,語法將如下所示:

object id_application = db.ExecuteScalar("procSaveApplication", xml)

Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false;

如果沒有 AutoDetectChangesEnabled = false,這些對速度影響太大; 我建議使用與 dbo 不同的表頭。 通常我使用像 nop、sop、tbl 等。

但是,對於超過(+4000)次插入,我建議使用存儲過程。 附上經過的時間。 我確實在 20" 中插入了 11.788 行在此處輸入圖片說明

這就是代碼

 public void InsertDataBase(MyEntity entity)
    {
        repository.Database.ExecuteSqlCommand("sp_mystored " +
                "@param1, @param2"
                 new SqlParameter("@param1", entity.property1),
                 new SqlParameter("@param2", entity.property2));
    }

使用此技術可以提高在Entity Framework中插入記錄的速度。 在這里,我使用一個簡單的存儲過程來插入記錄。 為了執行此存儲過程,我使用了執行原始SQL 的Entity Framework的.FromSql()方法

存儲過程的代碼:

CREATE PROCEDURE TestProc
@FirstParam VARCHAR(50),
@SecondParam VARCHAR(50)

AS
  Insert into SomeTable(Name, Address) values(@FirstParam, @SecondParam) 
GO

接下來,遍歷所有4000條記錄並添加執行存儲的Entity Framework代碼

程序每100次循環一次。

為此,我創建了一個字符串查詢來執行此過程,繼續將每組記錄追加到該字符串查詢。

然后檢查循環是否以100的倍數運行,在這種情況下,請使用.FromSql()執行.FromSql()

因此,對於4000條記錄,我只需要執行該過程僅4000/100 = 40次

檢查以下代碼:

string execQuery = "";
var context = new MyContext();
for (int i = 0; i < 4000; i++)
{
    execQuery += "EXEC TestProc @FirstParam = 'First'" + i + "'', @SecondParam = 'Second'" + i + "''";

    if (i % 100 == 0)
    {
        context.Student.FromSql(execQuery);
        execQuery = "";
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM