Wann sollte ich SaveChanges () aufrufen, wenn ich Tausende von Entity Framework-Objekten erstelle? (wie während eines Imports)


80

Ich führe einen Import durch, der bei jedem Lauf Tausende von Datensätzen enthält. Ich suche nur nach einer Bestätigung meiner Annahmen:

Welche davon ist am sinnvollsten:

  1. Führen Sie SaveChanges()jeden AddToClassName()Anruf aus.
  2. Führen Sie SaveChanges()jede n Anzahl der AddToClassName()Anrufe.
  3. Führen Sie SaveChanges()nach all der AddToClassName()Anrufe.

Die erste Option ist wahrscheinlich langsam, oder? Da es die EF-Objekte im Speicher analysieren muss, SQL generieren, etc.

Ich gehe davon aus, dass die zweite Option die beste aus beiden Welten ist, da wir einen Versuch abfangen SaveChanges()können und nur n Datensätze gleichzeitig verlieren , wenn einer von ihnen fehlschlägt. Speichern Sie möglicherweise jede Charge in einer Liste <>. Wenn der SaveChanges()Anruf erfolgreich ist, entfernen Sie die Liste. Wenn dies fehlschlägt, protokollieren Sie die Elemente.

Die letzte Option wäre wahrscheinlich auch sehr langsam, da sich jedes einzelne EF-Objekt bis SaveChanges()zum Aufruf im Speicher befinden müsste . Und wenn das Speichern fehlschlägt, wird nichts festgeschrieben, oder?

Antworten:


62

Ich würde es zuerst testen, um sicher zu sein. Die Leistung muss nicht so schlecht sein.

Wenn Sie alle Zeilen in einer Transaktion eingeben müssen, rufen Sie sie nach der gesamten AddToClassName-Klasse auf. Wenn Zeilen unabhängig eingegeben werden können, speichern Sie die Änderungen nach jeder Zeile. Die Datenbankkonsistenz ist wichtig.

Zweite Option mag ich nicht. Es wäre für mich (aus Sicht des Endbenutzers) verwirrend, wenn ich in das System importieren würde, und es würde 10 von 1000 Zeilen ablehnen, nur weil 1 schlecht ist. Sie können versuchen, 10 zu importieren. Wenn dies fehlschlägt, versuchen Sie es nacheinander und melden Sie sich dann an.

Testen Sie, ob es lange dauert. Schreiben Sie nicht "wahrscheinlich". Du weißt es noch nicht. Denken Sie nur dann an eine andere Lösung (marc_s), wenn es sich tatsächlich um ein Problem handelt.

BEARBEITEN

Ich habe einige Tests durchgeführt (Zeit in Millisekunden):

10000 Zeilen:

SaveChanges () nach 1 Zeile: 18510.534
SaveChanges () nach 100 Zeilen: 4350,3075
SaveChanges () nach 10000 Zeilen: 5233,0635

50000 Zeilen:

SaveChanges () nach 1 Zeile: 78496.929
SaveChanges () nach 500 Zeilen: 22302,2835
SaveChanges () nach 50000 Zeilen: 24022,8765

Es ist also tatsächlich schneller, nach n Zeilen festzuschreiben als nach allem.

Meine Empfehlung lautet:

  • SaveChanges () nach n Zeilen.
  • Wenn ein Commit fehlschlägt, versuchen Sie es nacheinander, um eine fehlerhafte Zeile zu finden.

Testklassen:

TABELLE:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Klasse:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

Der Grund, warum ich "wahrscheinlich" geschrieben habe, ist, dass ich eine fundierte Vermutung angestellt habe. Um klarer zu machen, dass "ich nicht sicher bin", habe ich es zu einer Frage gemacht. Ich denke auch, dass es durchaus sinnvoll ist, über mögliche Probleme nachzudenken, bevor ich auf sie stoße. Aus diesem Grund habe ich diese Frage gestellt. Ich hatte gehofft, jemand würde wissen, welche Methode am effizientesten ist, und ich könnte sofort damit anfangen.
John Bubriski

Super Kerl. Genau das, wonach ich gesucht habe. Vielen Dank, dass Sie sich die Zeit genommen haben, dies zu testen! Ich vermute, dass ich jeden Stapel im Speicher speichern, das Commit versuchen und dann, wenn es fehlschlägt, jeden einzeln durchlaufen kann, wie Sie gesagt haben. Sobald dieser Stapel fertig ist, geben Sie die Verweise auf diese 100 Elemente frei, damit sie aus dem Speicher gelöscht werden können. Danke noch einmal!
John Bubriski

3
Der Speicher wird nicht freigegeben, da alle Objekte von ObjectContext gehalten werden, aber 50000 oder 100000 im Kontext zu haben, nimmt heutzutage nicht viel Platz in Anspruch.
LukLed

6
Ich habe tatsächlich festgestellt, dass sich die Leistung zwischen jedem Aufruf von SaveChanges () verschlechtert. Die Lösung hierfür besteht darin, den Kontext nach jedem Aufruf von SaveChanges () tatsächlich zu entsorgen und einen neuen für den nächsten hinzuzufügenden Datenstapel erneut zu instanziieren.
Shawn de Wet

1
@LukLed nicht ganz ... Sie rufen SaveChanges in Ihrer For-Schleife auf ... damit der Code weitere Elemente hinzufügen kann, die in der for-Schleife auf derselben Instanz von ctx gespeichert werden sollen, und SaveChanges später auf derselben Instanz erneut aufrufen kann .
Shawn de Wet

18

Ich habe gerade ein sehr ähnliches Problem in meinem eigenen Code optimiert und möchte auf eine Optimierung hinweisen, die für mich funktioniert hat.

Ich habe festgestellt, dass die meiste Zeit bei der Verarbeitung von SaveChanges, unabhängig davon, ob 100 oder 1000 Datensätze gleichzeitig verarbeitet werden, an die CPU gebunden ist. Durch die Verarbeitung der Kontexte mit einem Producer / Consumer-Muster (implementiert mit BlockingCollection) konnte ich CPU-Kerne viel besser nutzen und kam von insgesamt 4000 Änderungen / Sekunde (wie durch den Rückgabewert von SaveChanges angegeben) auf über 14.000 Änderungen / Sekunde. Die CPU-Auslastung stieg von ca. 13% (ich habe 8 Kerne) auf ca. 60%. Selbst bei Verwendung mehrerer Consumer-Threads habe ich das (sehr schnelle) Festplatten-E / A-System kaum besteuert, und die CPU-Auslastung von SQL Server betrug nicht mehr als 15%.

Durch das Auslagern der Speicherung auf mehrere Threads können Sie sowohl die Anzahl der Datensätze vor dem Festschreiben als auch die Anzahl der Threads, die die Festschreibungsvorgänge ausführen, optimieren.

Ich habe festgestellt, dass ich durch das Erstellen von 1 Producer-Thread und (Anzahl der CPU-Kerne) -1 Consumer-Threads die Anzahl der pro Stapel festgeschriebenen Datensätze so einstellen konnte, dass die Anzahl der Elemente in der BlockingCollection zwischen 0 und 1 schwankte (nachdem ein Consumer-Thread einen genommen hatte Artikel). Auf diese Weise gab es gerade genug Arbeit, damit die konsumierenden Threads optimal funktionieren.

In diesem Szenario muss natürlich für jeden Stapel ein neuer Kontext erstellt werden, der selbst in einem Single-Thread-Szenario für meinen Anwendungsfall schneller ist.


Hallo, @ eric-j, könnten Sie bitte diese Zeile etwas näher erläutern, "indem Sie die Kontexte mit einem Produzenten- / Konsumentenmuster (implementiert mit BlockingCollection) verarbeiten", damit ich es mit meinem Code versuchen kann?
Foyzul Karim

14

Wenn Sie Tausende von Datensätzen importieren müssen, würde ich so etwas wie SqlBulkCopy verwenden und nicht das Entity Framework dafür.


15
Ich hasse es, wenn Leute meine Frage nicht beantworten :) Nun, sagen wir, ich "brauche" EF. Was dann?
John Bubriski

3
Nun, wenn Sie wirklich muss EF verwenden, dann würde ich versuchen , nach einer Charge von etwa 500 oder 1000 Datensätze zu begehen. Andernfalls werden Sie zu viele Ressourcen verbrauchen, und ein Fehler würde möglicherweise alle 99999 Zeilen zurücksetzen, die Sie aktualisiert haben, wenn die 100.000ste fehlschlägt.
marc_s

Mit dem gleichen Problem endete ich mit der Verwendung von SqlBulkCopy, das in diesem Fall viel leistungsfähiger ist als EF. Obwohl ich nicht gerne mehrere Möglichkeiten benutze, um auf die Datenbank zuzugreifen.
Julien N

2
Ich prüfe auch diese Lösung, da ich das gleiche Problem habe ... Eine Massenkopie wäre eine ausgezeichnete Lösung, aber mein Hosting-Service verbietet die Verwendung (und ich würde vermuten, dass dies auch andere tun würden), so dass dies nicht praktikabel ist Option für einige Leute.
Dennis Ward

3
@marc_s: Wie gehen Sie mit der Notwendigkeit um, Geschäftsregeln, die Geschäftsobjekten inhärent sind, bei Verwendung von SqlBulkCopy durchzusetzen? Ich sehe nicht ein, wie man EF nicht verwendet, ohne die Regeln redundant zu implementieren.
Eric J.

2

Verwenden Sie eine gespeicherte Prozedur.

  1. Erstellen Sie einen benutzerdefinierten Datentyp in SQL Server.
  2. Erstellen und füllen Sie ein Array dieses Typs in Ihrem Code (sehr schnell).
  3. Übergeben Sie das Array mit einem Aufruf (sehr schnell) an Ihre gespeicherte Prozedur.

Ich glaube, dies wäre der einfachste und schnellste Weg, dies zu tun.


7
In der Regel müssen bei SO die Behauptungen "Dies ist am schnellsten" mit Testcode und Ergebnissen belegt werden.
Michael Blackburn

2

Entschuldigung, ich weiß, dass dieser Thread alt ist, aber ich denke, dies könnte anderen Menschen bei diesem Problem helfen.

Ich hatte das gleiche Problem, aber es besteht die Möglichkeit, die Änderungen zu überprüfen, bevor Sie sie festschreiben. Mein Code sieht so aus und funktioniert einwandfrei. Mit dem chUser.LastUpdatedüberprüfe ich, ob es sich um einen neuen Eintrag oder nur um eine Änderung handelt. Weil es nicht möglich ist, einen Eintrag neu zu laden, der sich noch nicht in der Datenbank befindet.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();

Ja, es geht um das gleiche Problem, oder? Mit dieser Option können Sie alle 1000 Datensätze hinzufügen und vor der Ausführung saveChanges()diejenigen löschen, die einen Fehler verursachen würden.
Jan Leuenberger

1
Der Schwerpunkt der Frage liegt jedoch darauf, wie viele Einfügungen / Aktualisierungen in einem SaveChangesAufruf effizient festgeschrieben werden sollen. Sie sprechen dieses Problem nicht an. Beachten Sie, dass es mehr mögliche Gründe für das Fehlschlagen von SaveChanges gibt als Validierungsfehler. Übrigens können Sie Entitäten auch einfach als neu markieren Unchanged/ löschen.
Gert Arnold

1
Sie haben Recht, es geht nicht direkt auf die Frage ein, aber ich denke, die meisten Leute, die über diesen Thread stolpern, haben das Problem mit der Validierung, obwohl es andere Gründe gibt, die SaveChangesfehlschlagen. Und das löst das Problem. Wenn dieser Beitrag Sie in diesem Thread wirklich stört, kann ich dies löschen, mein Problem ist gelöst, ich versuche nur, anderen zu helfen.
Jan Leuenberger

Ich habe eine Frage zu diesem. Wenn Sie anrufen GetValidationErrors(), "fälscht" es einen Aufruf der Datenbank und ruft Fehler ab oder was? Vielen Dank für
Ihre
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.