c# tutorial Schnellste Art des Einfügens in Entity Framework




entity framework tutorial deutsch (20)

Ich bin auf der Suche nach dem schnellsten Weg zum Einfügen in Entity Framework.

Ich frage das wegen des Szenarios, in dem Sie ein aktives TransactionScope haben und die Einfügung ist riesig (4000+). Es kann möglicherweise länger als 10 Minuten dauern (Standard-Timeout von Transaktionen) und dies führt zu einer unvollständigen Transaktion.



Nach meinem Wissen gibt es no BulkInsert in EntityFramework , um die Leistung der riesigen Einsätze zu erhöhen.

In diesem Szenario können Sie mit documentation in ADO.net , um Ihr Problem zu lösen


Ich bin auf der Suche nach dem schnellsten Weg zum Einfügen in Entity Framework

Es gibt einige Third-Party-Bibliotheken, die Bulk Insert unterstützen:

  • Z.EntityFramework.Extensions ( Empfohlen )
  • EFutilities
  • EntityFramework.BulkInsert

Siehe: Entity Framework-Masseneinfügungsbibliothek

Seien Sie vorsichtig, wenn Sie eine Masseneinsatzbibliothek auswählen. Nur Entity Framework Extensions unterstützen alle Arten von Assoziationen und Vererbung und es ist die einzige, die noch unterstützt wird.

Haftungsausschluss : Ich bin der Eigentümer von Entity Framework Extensions

Mit dieser Bibliothek können Sie alle Massenvorgänge ausführen, die Sie für Ihre Szenarien benötigen:

  • Bulk SaveÄnderungen
  • Masseneinfügung
  • Massenlöschung
  • Bulk-Update
  • Massenzusammenführung

Beispiel

// 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;
});

Zu Ihrer Bemerkung in den Kommentaren zu Ihrer Frage:

"... SavingChanges ( für jeden Datensatz ) ..."

Das ist das Schlimmste, was du tun kannst! Durch den Aufruf von SaveChanges() für jeden Datensatz werden SaveChanges() extrem SaveChanges() . Ich würde ein paar einfache Tests machen, die sehr wahrscheinlich die Leistung verbessern werden:

  • Rufen Sie SaveChanges() einmal nach SaveChanges() Datensätzen auf.
  • Rufen Sie SaveChanges() nach beispielsweise 100 Datensätzen auf.
  • Rufen Sie SaveChanges() nach zB 100 SaveChanges() auf und disponieren Sie den Kontext und erstellen Sie einen neuen.
  • Deaktivieren Sie die Änderungserkennung

Für Masseneinsätze arbeite und experimentiere ich mit einem Muster wie diesem:

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;
}

Ich habe ein Testprogramm, das 560.000 Entitäten (9 skalare Eigenschaften, keine Navigationseigenschaften) in die DB einfügt. Mit diesem Code funktioniert es in weniger als 3 Minuten.

Für die Performance ist es wichtig, SaveChanges() nach "vielen" Records ("viele" um 100 oder 1000) SaveChanges() . Es verbessert auch die Leistung, um den Kontext nach SaveChanges zu entfernen und einen neuen zu erstellen. Dies löscht den Kontext von allen Entitäten, SaveChanges macht das nicht, die Entitäten sind immer noch an den Kontext im Status Unchanged SaveChanges . Es ist die wachsende Größe von angehängten Entitäten im Kontext, was die Einfügung Schritt für Schritt verlangsamt. Es ist also hilfreich, es nach einiger Zeit zu löschen.

Hier sind ein paar Messungen für meine 560.000 Einheiten:

  • commitCount = 1, recreateContext = false: viele Stunden (Das ist Ihre aktuelle Prozedur)
  • commitCount = 100, recreateContext = false: mehr als 20 Minuten
  • commitCount = 1000, recreateContext = false: 242 Sek
  • commitCount = 10000, recreateContext = false: 202 Sek
  • commitCount = 100000, recreateContext = false: 199 Sek
  • commitCount = 1000000, recreateContext = false: Ausnahme wegen zu wenig Arbeitsspeicher
  • commitCount = 1, recreateContext = true: mehr als 10 Minuten
  • commitCount = 10, recreateContext = true: 241 Sek
  • commitCount = 100, recreateContext = true: 164 Sek
  • commitCount = 1000, recreateContext = true: 191 Sek

Das Verhalten im ersten Test oben ist, dass die Leistung sehr nichtlinear ist und im Laufe der Zeit extrem abnimmt. ("Viele Stunden" ist eine Schätzung, ich habe diesen Test nie beendet, ich habe bei 50.000 Einheiten nach 20 Minuten angehalten.) Dieses nichtlineare Verhalten ist in allen anderen Tests nicht so signifikant.


Ich habe eine generische Erweiterung von @Slaumas Beispiel oben gemacht;

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;
    }
}

Verwendung:

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();
    }
}

Wie andere Leute gesagt haben, SqlBulkCopy ist der Weg, es zu tun, wenn Sie wirklich gute Insert-Leistung wollen.

Es ist ein wenig umständlich zu implementieren, aber es gibt Bibliotheken, die Ihnen dabei helfen können. Es gibt ein paar da draußen, aber ich werde diesmal meine eigene Bibliothek schamlos pluggen: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

Der einzige Code, den Sie benötigen, ist:

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

Also, wie viel schneller ist es? Sehr schwer zu sagen, weil es von so vielen Faktoren, Computerleistung, Netzwerk, Objektgröße usw. abhängt. Die Leistungstests, die ich gemacht habe, schlagen vor, dass 25k Entities um 10s den Standardweg auf localhost eingefügt werden können, wenn Sie Ihre EF Konfiguration optimieren in den anderen Antworten erwähnt. Mit EFUtilities dauert das ungefähr 300ms. Noch interessanter ist, dass ich mit dieser Methode ungefähr 3 Millionen Entitäten in weniger als 15 Sekunden gespeichert habe, durchschnittlich etwa 200.000 Einheiten pro Sekunde.

Das einzige Problem ist natürlich, wenn Sie relevante Daten einfügen müssen. Dies kann mit der oben beschriebenen Methode effizient in SQL Server durchgeführt werden, erfordert jedoch eine Strategie zur ID-Generierung, mit der Sie IDs im App-Code für das Elternelement generieren können, sodass Sie die Fremdschlüssel festlegen können. Dies kann mit GUIDs oder etwas wie HiLo-ID-Generierung erfolgen.


Ich weiß, das ist eine sehr alte Frage, aber ein Typ hier sagte, dass eine Erweiterungs-Methode entwickelt, um Massen einfügen mit EF zu verwenden, und als ich überprüfte, entdeckte ich, dass die Bibliothek $ 599 heute kostet (für einen Entwickler). Vielleicht macht es Sinn für die gesamte Bibliothek, aber für den Masseneinsatz ist das zu viel.

Hier ist eine sehr einfache Erweiterungsmethode, die ich gemacht habe. Ich benutze das zuerst mit der Datenbank (zuerst nicht mit Code getestet, aber ich denke, das funktioniert genauso). Ändern Sie YourEntities mit dem Namen Ihres Kontexts:

public partial class YourEntities : DbContext
{
    public async Task BulkInsertAllAsync<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;
            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;
    }
}

Sie können das für jede Sammlung verwenden, die von IEnumerable erbt:

await context.BulkInsertAllAsync(items);

Haben Sie jemals versucht, durch einen Hintergrundarbeiter oder eine Aufgabe einzufügen?

In meinem Fall, im Einfügen von 7760 Registern, verteilt in 182 verschiedenen Tabellen mit Fremdschlüsselbeziehungen (von NavigationProperties).

Ohne die Aufgabe dauerte es zweieinhalb Minuten. Innerhalb einer Task ( Task.Factory.StartNew(...) ) dauerte es 15 Sekunden.

SaveChanges() mache nur die SaveChanges() nach dem Hinzufügen aller Entitäten zum Kontext. (um die Datenintegrität zu gewährleisten)


Sie sollten sich die System.Data.SqlClient.SqlBulkCopy dafür ansehen. Hier ist die documentation , und natürlich gibt es viele Tutorials online.

Sorry, ich weiß, dass Sie nach einer einfachen Antwort gesucht haben, um EF dazu zu bringen, das zu tun, was Sie wollen, aber Bulk-Operationen sind nicht wirklich das, wozu ORMs gedacht sind.


Versuchen Sie eine Stored Procedure zu verwenden , die ein XML der Daten, die Sie einfügen möchten, erhält.


Use stored procedure that takes input data in form of xml to insert data.

From your c# code pass insert data as xml.

eg in c#, syntax would be like this:

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

Im Folgenden finden Sie einen Leistungsvergleich zwischen der Verwendung von Entity Framework und der Verwendung der SqlBulkCopy-Klasse für ein realistisches Beispiel: So fügen Sie komplexe Objekte in der SQL Server-Datenbank in Massenform ein

Wie andere bereits betont haben, sind ORMs nicht für den Massenbetrieb gedacht. Sie bieten Flexibilität, Trennung von Anliegen und andere Vorteile, aber Bulk-Operationen (außer Bulk-Lesen) gehören nicht dazu.


Ich würde diesen Artikel empfehlen, wie man Masseneinfügungen mit EF macht.

Entity Framework und langsame Bulk-INSERTs

Er erforscht diese Bereiche und vergleicht die Leistung:

  1. Standard-EF (57 Minuten zum Hinzufügen von 30.000 Datensätzen)
  2. Ersetzen mit ADO.NET Code (25 Sekunden für die gleichen 30.000)
  3. Context Bloat: Behalten Sie den aktiven Context Graph klein, indem Sie für jede Arbeitseinheit einen neuen Kontext verwenden (die gleichen 30.000 Inserts benötigen 33 Sekunden)
  4. Große Listen - Deaktivieren Sie AutoDetectChangesEnabled (verringert die Zeit auf ca. 20 Sekunden)
  5. Batching (bis zu 16 Sekunden)
  6. DbTable.AddRange () - (Leistung liegt im Bereich 12)

Eine weitere Option ist die Verwendung von SqlBulkTools, die von Nuget verfügbar sind. Es ist sehr einfach zu bedienen und hat einige leistungsstarke Funktionen.

Beispiel:

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();
}

Weitere Beispiele und erweiterte Verwendung finden Sie in der Dokumentation . Disclaimer: Ich bin der Autor dieser Bibliothek und jede Meinung ist meiner Meinung nach.


Der Dispose () - Kontext erzeugt Probleme, wenn die Entitäten, die Sie hinzufügen (), im Kontext auf andere vorgeladene Entitäten (zB Navigationseigenschaften) angewiesen sind

Ich verwende ein ähnliches Konzept, um meinen Kontext klein zu halten, um die gleiche Leistung zu erzielen

Aber statt Dispose () den Kontext und neu erstellen, ich einfach die Entitäten, die bereits 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;
} }

Wickeln Sie es mit try catch und TrasactionScope (), wenn Sie es brauchen, nicht hier, um den Code sauber zu halten


Diese Kombination erhöht die Geschwindigkeit gut genug.

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

Der schnellste Weg wäre die Verwendung der Bulk-Insert-Erweiterung , die ich entwickelt habe.

Es verwendet SqlBulkCopy und benutzerdefinierten Datenreader, um maximale Leistung zu erhalten. Als Ergebnis ist es über 20 mal schneller als normale Einfügung oder AddRange

Die Verwendung ist sehr einfach

context.BulkInsert(hugeAmountOfEntities);

But, for more than (+4000) inserts i recommend to use stored procedure. attached the time elapsed. I did inserted it 11.788 rows in 20"

thats it code

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

[NEW SOLUTION FOR POSTGRESQL] Hey, I know it's quite an old post, but I have recently run into similar problem, but we were using Postgresql. I wanted to use effective bulkinsert, what turned out to be pretty difficult. I haven't found any proper free library to do so on this DB. I have only found this helper: https://bytefish.de/blog/postgresql_bulk_insert/ which is also on Nuget. I have written a small mapper, which auto mapped properties the way Entity Framework:

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;
        }

I use it the following way (I had entity named Undertaking):

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

I showed an example with transaction, but it can also be done with normal connection retrieved from context. undertakingsToAdd is enumerable of normal entity records, which I want to bulkInsert into DB.

This solution, to which I've got after few hours of research and trying, is as you could expect much faster and finally easy to use and free! I really advice you to use this solution, not only for the reasons mentioned above, but also because it's the only one with which I had no problems with Postgresql itself, many other solutions work flawlessly for example with SqlServer.


Ich habe die Antwort von Slauma untersucht (was super ist, danke für den Ideenmenschen), und ich habe die Losgröße reduziert, bis ich die optimale Geschwindigkeit erreicht habe. Blick auf die Ergebnisse von Slauma:

  • commitCount = 1, recreateContext = true: mehr als 10 Minuten
  • commitCount = 10, recreateContext = true: 241 Sek
  • commitCount = 100, recreateContext = true: 164 Sek
  • commitCount = 1000, recreateContext = true: 191 Sek

Es ist sichtbar, dass die Geschwindigkeit zunimmt, wenn von 1 auf 10 und von 10 auf 100 gewechselt wird, aber von 100 auf 1000 fällt die Einsetzgeschwindigkeit wieder ab.

Ich habe mich also darauf konzentriert, was passiert, wenn Sie die Batchgröße auf einen Wert zwischen 10 und 100 reduzieren, und hier sind meine Ergebnisse (ich verwende unterschiedliche Zeileninhalte, so dass meine Zeiten unterschiedlich sind):

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

Basierend auf meinen Ergebnissen liegt das tatsächliche Optimum bei 30 für die Chargengröße. Es ist weniger als 10 und 100. Das Problem ist, ich habe keine Ahnung, warum ist 30 optimal, noch konnte ich eine logische Erklärung dafür finden.







entity-framework