Kiedy należy wywołać metodę SaveChanges () podczas tworzenia tysięcy obiektów Entity Framework? (jak podczas importu)


80

Prowadzę import, który będzie miał tysiące rekordów w każdym przebiegu. Szukam tylko potwierdzenia moich przypuszczeń:

Który z poniższych jest najbardziej sensowny:

  1. Uruchom SaveChanges()każde AddToClassName()połączenie.
  2. Uruchom SaveChanges()co n liczbę AddToClassName()połączeń.
  3. Uruchomić SaveChanges()po wszystkim z AddToClassName()połączeniami.

Pierwsza opcja jest prawdopodobnie wolna, prawda? Ponieważ będzie musiał przeanalizować obiekty EF w pamięci, wygenerować SQL itp.

Zakładam, że druga opcja jest najlepsza z obu światów, ponieważ możemy owinąć próbę złapania wokół tego SaveChanges()połączenia i stracić tylko n liczby rekordów na raz, jeśli jeden z nich zawiedzie. Może przechowywać każdą partię na liście <>. Jeśli SaveChanges()połączenie się powiedzie, pozbądź się listy. Jeśli to się nie powiedzie, zarejestruj elementy.

Ostatnia opcja prawdopodobnie również byłaby bardzo powolna, ponieważ każdy pojedynczy obiekt EF musiałby znajdować się w pamięci, dopóki nie SaveChanges()zostanie wywołany. A jeśli zapis się nie powiedzie, nic nie zostanie popełnione, prawda?

Odpowiedzi:


62

Najpierw przetestowałbym to, aby mieć pewność. Wydajność nie musi być taka zła.

Jeśli chcesz wprowadzić wszystkie wiersze w jednej transakcji, wywołaj ją po całej klasie AddToClassName. Jeśli wiersze można wprowadzać niezależnie, zapisz zmiany po każdym wierszu. Spójność bazy danych jest ważna.

Druga opcja mi się nie podoba. Byłoby dla mnie mylące (z punktu widzenia użytkownika końcowego), gdybym dokonał importu do systemu i zmniejszyłby się o 10 wierszy na 1000, tylko dlatego, że 1 jest zły. Możesz spróbować zaimportować 10, a jeśli się to nie powiedzie, spróbuj pojedynczo, a następnie zaloguj się.

Sprawdź, czy zajmuje to dużo czasu. Nie pisz „prawdopodobnie”. Jeszcze tego nie wiesz. Dopiero gdy jest to rzeczywiście problem, pomyśl o innym rozwiązaniu (marc_s).

EDYTOWAĆ

Zrobiłem kilka testów (czas w milisekundach):

10000 rzędów:

SaveChanges () po 1 wierszu: 18510,534
SaveChanges () after 100 wierszy: 4350,3075
SaveChanges () after 10000 wierszy: 5233,0635

50000 rzędów:

SaveChanges () after 1 row: 78496,929
SaveChanges () after 500 wierszy: 22302,2835
SaveChanges () after 50000 wierszy: 24022,8765

Tak więc zatwierdzenie po n wierszach jest w rzeczywistości szybsze niż w końcu.

Polecam:

  • SaveChanges () po n wierszach.
  • Jeśli jedno zatwierdzenie nie powiedzie się, wypróbuj je jeden po drugim, aby znaleźć wadliwy wiersz.

Klasy testowe:

STÓŁ:

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]

Klasa:

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

Powodem, dla którego napisałem „prawdopodobnie”, jest to, że zgadnąłem. Aby wyjaśnić, że „nie jestem pewien”, ułożyłem to w pytanie. Myślę też, że warto pomyśleć o potencjalnych problemach, ZANIM na nie wpadnę. To jest powód, dla którego zadałem to pytanie. Miałem nadzieję, że ktoś będzie wiedział, która metoda będzie najbardziej skuteczna, i mogłem to zrobić od razu.
John Bubriski

Niesamowity koleś. Dokładnie to, czego szukałem. Dziękujemy za poświęcenie czasu na przetestowanie tego! Zgaduję, że mogę przechowywać każdą partię w pamięci, wypróbować zatwierdzenie, a jeśli się nie powiedzie, przejdę przez każdą z nich indywidualnie, jak powiedziałeś. Następnie, po zakończeniu tej partii, zwolnij odniesienia do tych 100 pozycji, aby można je było usunąć z pamięci. Dzięki jeszcze raz!
John Bubriski

3
Pamięć nie zostanie zwolniona, ponieważ wszystkie obiekty będą przechowywane przez ObjectContext, ale posiadanie 50000 lub 100000 w kontekście nie zajmuje obecnie dużo miejsca.
LukLed

6
Właściwie odkryłem, że wydajność spada między każdym wywołaniem metody SaveChanges (). Rozwiązaniem tego problemu jest faktyczne usunięcie kontekstu po każdym wywołaniu SaveChanges () i ponowne utworzenie nowej instancji dla następnej partii danych do dodania.
Shawn de Wet

1
@LukLed niezupełnie ... wywołujesz SaveChanges w swojej pętli For ... więc kod może kontynuować dodawanie kolejnych elementów do zapisania w pętli for w tej samej instancji ctx i później ponownie wywołać SaveChanges w tej samej instancji .
Shawn de Wet

18

Właśnie zoptymalizowałem bardzo podobny problem we własnym kodzie i chciałbym wskazać optymalizację, która zadziałała dla mnie.

Zauważyłem, że większość czasu podczas przetwarzania SaveChanges, niezależnie od tego, czy przetwarzamy 100 czy 1000 rekordów jednocześnie, jest związana z procesorem. Tak więc, przetwarzając konteksty za pomocą wzorca producenta / konsumenta (zaimplementowanego za pomocą BlockingCollection), byłem w stanie znacznie lepiej wykorzystać rdzenie procesora i uzyskałem z łącznie 4000 zmian na sekundę (zgodnie z wartością zwracaną SaveChanges) do ponad 14 000 zmian na sekundę. Wykorzystanie procesora zmieniło się z około 13% (mam 8 rdzeni) do około 60%. Nawet używając wielu wątków konsumenckich, ledwo opodatkowałem (bardzo szybki) system IO dysku, a wykorzystanie procesora SQL Server nie było wyższe niż 15%.

Odciążając zapisywanie do wielu wątków, masz możliwość dostrojenia zarówno liczby rekordów przed zatwierdzeniem, jak i liczby wątków wykonujących operacje zatwierdzania.

Okazało się, że utworzenie 1 wątku producenta i (liczby rdzeni procesora) -1 wątków konsumenckich pozwoliło mi dostroić liczbę rekordów zatwierdzonych na partię tak, że liczba elementów w BlockingCollection wahała się między 0 a 1 (po tym, jak wątek konsumencki wziął jeden pozycja). W ten sposób było wystarczająco dużo pracy, aby zużywające wątki działały optymalnie.

Ten scenariusz wymaga oczywiście stworzenia nowego kontekstu dla każdej partii, co jest szybsze nawet w przypadku scenariusza jednowątkowego dla mojego przypadku użycia.


Cześć, @ eric-j, czy mógłbyś nieco rozwinąć ten wiersz „przetwarzając konteksty za pomocą wzorca producenta / konsumenta (zaimplementowanego za pomocą BlockingCollection)”, abym mógł spróbować z moim kodem?
Foyzul Karim

14

Jeśli chcesz zaimportować tysiące rekordów, użyłbym czegoś takiego jak SqlBulkCopy, a nie Entity Framework.


15
Nienawidzę, gdy ludzie nie odpowiadają na moje pytanie :) Cóż, powiedzmy, że „potrzebuję” EF. Co wtedy?
John Bubriski

3
Cóż, jeśli naprawdę MUSISZ użyć EF, spróbuję zatwierdzić po partii, powiedzmy, 500 lub 1000 rekordów. W przeciwnym razie zużyjesz zbyt dużo zasobów, a awaria może potencjalnie cofnąć wszystkie zaktualizowane wiersze 99999, gdy 100000 nie powiedzie się.
marc_s

Z tym samym problemem zakończyłem, używając SqlBulkCopy, który w tym przypadku jest o wiele bardziej wydajny niż EF. Chociaż nie lubię korzystać z kilku sposobów dostępu do bazy danych.
Julien N

2
Patrzę też na to rozwiązanie, ponieważ mam ten sam problem ... Kopia zbiorcza byłaby doskonałym rozwiązaniem, ale moja usługa hostingowa nie pozwala z tego korzystać (i myślę, że inni też), więc nie jest to opłacalne opcja dla niektórych osób.
Dennis Ward

3
@marc_s: Jak radzisz sobie z potrzebą wymuszania reguł biznesowych właściwych dla obiektów biznesowych podczas korzystania z SqlBulkCopy? Nie wiem, jak nie używać EF bez nadmiarowego wdrażania reguł.
Eric J.

2

Użyj procedury składowanej.

  1. Utwórz typ danych zdefiniowany przez użytkownika na serwerze Sql.
  2. Utwórz i zapełnij tablicę tego typu w swoim kodzie (bardzo szybko).
  3. Przekaż tablicę do procedury składowanej jednym wywołaniem (bardzo szybko).

Uważam, że byłby to najłatwiejszy i najszybszy sposób na zrobienie tego.


7
Zazwyczaj w przypadku SO twierdzenia, że ​​„to jest najszybsze”, muszą być poparte kodem testu i wynikami.
Michael Blackburn

2

Przepraszam, wiem, że ten wątek jest stary, ale myślę, że może to pomóc innym osobom z tym problemem.

Miałem ten sam problem, ale istnieje możliwość sprawdzenia poprawności zmian przed ich zatwierdzeniem. Mój kod wygląda tak i działa dobrze. Przy pomocy chUser.LastUpdatedsprawdzam czy to nowy wpis czy tylko zmiana. Ponieważ nie jest możliwe ponowne załadowanie wpisu, którego nie ma jeszcze w bazie danych.

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

Tak, chodzi o ten sam problem, prawda? Dzięki temu możesz dodać wszystkie 1000 rekordów, a przed uruchomieniem saveChanges()możesz usunąć te, które spowodowałyby błąd.
Jan Leuenberger

1
Ale nacisk kładziony jest na pytanie, ile wstawek / aktualizacji ma zostać skutecznie zatwierdzonych w jednym SaveChangeswywołaniu. Nie rozwiązujesz tego problemu. Zauważ, że istnieje więcej potencjalnych przyczyn niepowodzenia SaveChanges niż błędów walidacji. Nawiasem mówiąc, możesz po prostu oznaczyć elementy jako Unchangedzamiast ich ponownego wczytywania / usuwania.
Gert Arnold

1
Masz rację, nie odnosi się to bezpośrednio do pytania, ale myślę, że większość ludzi, którzy natknęli się na ten wątek, ma problem z walidacją, chociaż są inne przyczyny SaveChangesniepowodzenia. I to rozwiązuje problem. Jeśli ten post naprawdę Ci przeszkadza w tym wątku mogę to usunąć, mój problem został rozwiązany, po prostu próbuję pomóc innym.
Jan Leuenberger

Mam pytanie dotyczące tego. Kiedy dzwonisz, GetValidationErrors()czy to „fałszywe” wywołanie bazy danych i wyszukuje błędy, czy co? Dzięki za odpowiedź :)
Jeancarlo Fontalvo
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.