Najszybszy sposób wstawiania w Entity Framework

682

Szukam najszybszego sposobu wstawienia do Entity Framework.

Pytam o to ze względu na scenariusz, w którym masz aktywny TransactionScope, a wstawienie jest ogromne (4000+). Może to potencjalnie trwać dłużej niż 10 minut (domyślny limit czasu transakcji), co doprowadzi do niekompletnej transakcji.

Bongo Sharp
źródło
1
Jak obecnie to robisz?
Dustin Laine,
Tworzenie TransactionScope, tworzenie instancji DBContext, otwieranie połączenia oraz w każdej instrukcji zawierającej wstawki i SavingChanges (dla każdego rekordu), UWAGA: TransactionScope i DBContext używają instrukcji i ostatecznie zamykam połączenie blok
Bongo Sharp
Inna odpowiedź w celach informacyjnych: stackoverflow.com/questions/5798646/…
Ladislav Mrnka
2
Najszybszy sposób wstawiania do bazy danych SQL nie wymaga EF. AFAIK Jego BCP, a następnie TVP + Merge / insert.
StingyJack
1
Dla tych, którzy będą czytać komentarze: Najbardziej odpowiednia, nowoczesna odpowiedź jest tutaj.
Tanveer Badar

Odpowiedzi:

986

Do Twojej uwagi w komentarzach do twojego pytania:

„... Zapisywanie zmian ( dla każdego rekordu ) ...”

To najgorsze, co możesz zrobić! Wezwanie SaveChanges()do każdego rekordu spowalnia bardzo duże wkładki luzem. Zrobiłbym kilka prostych testów, które prawdopodobnie poprawią wydajność:

  • Zadzwoń SaveChanges()raz po WSZYSTKICH rekordach.
  • Zadzwoń SaveChanges()po na przykład 100 rekordów.
  • Przywołaj SaveChanges()na przykład 100 rekordów i usuń kontekst i utwórz nowy.
  • Wyłącz wykrywanie zmian

W przypadku wkładek luzem pracuję i eksperymentuję z takim wzorem:

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

Mam program testowy, który wstawia 560 000 jednostek (9 właściwości skalarnych, brak właściwości nawigacyjnych) do DB. Z tym kodem działa w mniej niż 3 minuty.

Dla wykonania ważne jest, aby wywoływać SaveChanges()po „wielu” rekordach („wielu” około 100 lub 1000). Poprawia także wydajność, aby pozbyć się kontekstu po SaveChanges i utworzyć nowy. To usuwa kontekst ze wszystkich entites, SaveChangesnie robi tego, byty są nadal dołączone do kontekstu w stanie Unchanged. Rosnące rozmiary dołączonych elementów w kontekście spowalniają wstawianie krok po kroku. Pomocne jest więc wyczyszczenie go po pewnym czasie.

Oto kilka pomiarów dla moich 560000 podmiotów:

  • commitCount = 1, createateContext = false: many hours (To jest twoja obecna procedura)
  • commitCount = 100, createateContext = false: ponad 20 minut
  • commitCount = 1000, createateContext = false: 242 sec
  • commitCount = 10000, createateContext = false: 202 sec
  • commitCount = 100000, createateContext = false: 199 sec
  • commitCount = 1000000, createateContext = false: wyjątek braku pamięci
  • commitCount = 1, createateContext = true: ponad 10 minut
  • commitCount = 10, createateContext = true: 241 sec
  • commitCount = 100, createateContext = true: 164 sec
  • commitCount = 1000, createateContext = true: 191 sec

Zachowanie w pierwszym powyższym teście polega na tym, że wydajność jest bardzo nieliniowa i z czasem bardzo spada. („Wiele godzin” to oszacowanie, nigdy nie ukończyłem tego testu, po 20 minutach zatrzymałem się na 50 000 jednostek). To nieliniowe zachowanie nie jest tak znaczące we wszystkich innych testach.

Slauma
źródło
89
@ Bongo Sharp: Nie zapomnij ustawić AutoDetectChangesEnabled = false;w DbContext. Ma także duży dodatkowy efekt wydajnościowy: stackoverflow.com/questions/5943394/...
Slauma,
6
Tak, problem polega na tym, że używam Entity Framework 4, a AutoDetectChangesEnabled jest częścią 4.1, niemniej jednak zrobiłem test wydajności i miałem niesamowite rezultaty, zmieniło się z 00:12:00 na 00:00:22 SavinChanges na każdej jednostce robił olverload ... DZIĘKUJĘ za twoje answare! tego właśnie szukałem
Bongo Sharp
10
Dziękujemy za kontekst.Configuration.AutoDetectChangesEnabled = false; wskazówka, robi to ogromną różnicę.
douglaz
1
@ dahacker89: Czy używasz poprawnej wersji EF> = 4.1 i DbContextNIE ObjectContext?
Slauma,
3
@ dahacker89: Sugeruję, abyś utworzył osobne pytanie dla swojego problemu, z bardziej szczegółowymi informacjami. Nie jestem w stanie dowiedzieć się, co jest nie tak.
Slauma,
176

Ta kombinacja wystarczająco dobrze zwiększa prędkość.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
arkhivania
źródło
46
Nie wyłączaj na ślepo ValidateOnSaveEnabled, możesz być zależny od tego zachowania i nie zdawaj sobie z tego sprawy, dopóki nie będzie za późno. Z drugiej strony możesz sprawdzać poprawność w innym miejscu w kodzie, a sprawdzanie poprawności EF po raz kolejny jest całkowicie niepotrzebne.
Jeremy Cook
1
W moim teście oszczędność 20 000 wierszy spadła ze 101 sekund do 88 sekund. Niewiele i jakie są tego konsekwencje.
AH.
27
@JeremyCook Myślę, że to, na co próbujesz się dostać, to odpowiedź byłaby znacznie lepsza, gdyby wyjaśniła możliwe konsekwencje zmiany tych właściwości od ich wartości domyślnych (oprócz poprawy wydajności). Zgadzam się.
pseudokoder
1
To działało dla mnie, chociaż jeśli aktualizujesz rekordy w kontekście, musisz jawnie wywołać DetectChanges ()
hillstuk
2
Można je wyłączyć, a następnie ponownie włączyć za pomocą bloku wypróbowania na końcu: msdn.microsoft.com/en-us/data/jj556205.aspx
yellavon
98

Najszybszym sposobem byłoby użycie rozszerzenia wkładki luzem , które opracowałem

Uwaga: jest to produkt komercyjny, nie bezpłatnie

Wykorzystuje SqlBulkCopy i niestandardowy moduł danych, aby uzyskać maksymalną wydajność. W rezultacie jest ponad 20 razy szybszy niż zwykłe wstawianie lub AddRange EntityFramework.BulkInsert vs. EF AddRange

użycie jest niezwykle proste

context.BulkInsert(hugeAmountOfEntities);
maxlego
źródło
10
Szybko, ale robi to tylko górna warstwa hierarchii.
CAD CAD
65
To nie jest za darmo.
Amir Saniyan
72
Reklamy stają się mądrzejsze ... jest to płatny produkt i bardzo drogi dla niezależnych. Być ostrzeżonym!
JulioQc,
35
600 USD za roczną pomoc techniczną i aktualizacje? Postradałeś rozum?
Camilo Terevinto,
7
nie
jestem
83

Powinieneś spojrzeć na użycie System.Data.SqlClient.SqlBulkCopydo tego. Oto dokumentacja i oczywiście jest wiele samouczków online.

Przykro mi, wiem, że szukałeś prostej odpowiedzi, aby EF mógł zrobić to, co chcesz, ale operacje masowe nie są tak naprawdę przeznaczone do ORM.

Adam Rackis
źródło
1
Kilka razy natknąłem się na SqlBulkCopy podczas badań, ale wydaje się, że jest bardziej zorientowany na wstawki między stołami, niestety nie spodziewałem się łatwych rozwiązań, ale raczej wskazówek dotyczących wydajności, takich jak na przykład zarządzanie stanem połączenie ręczne, zamiast pozwolić EF zrobić to za Ciebie
Bongo Sharp
7
Użyłem SqlBulkCopy, aby wstawić duże ilości danych bezpośrednio z mojej aplikacji. Zasadniczo trzeba utworzyć DataTable, wypełnić go, a następnie przekazać , że do bulkcopy. Podczas konfigurowania
tabeli danych jest kilka błędów
2
Zrobiłem dowód koncepcji i, jak obiecałem, działa naprawdę szybko, ale jednym z powodów, dla których używam EF, jest to, że wstawianie danych relacyjnych jest łatwiejsze, np. Jeśli wstawię encję, która już zawiera dane relacyjne , to również wstawi, czy kiedykolwiek miałeś do tego scenariusza? Dzięki!
Bongo Sharp
2
Niestety wstawianie sieci obiektów do DBMS nie jest tak naprawdę działaniem BulkCopy. Taka jest korzyść z ORM, takiego jak EF, kosztem jest to, że nie skaluje się, aby efektywnie wykonywać setki podobnych wykresów obiektowych.
Adam Rackis,
2
SqlBulkCopy jest zdecydowanie najlepszym rozwiązaniem, jeśli potrzebujesz surowej prędkości lub jeśli ponownie uruchomisz tę wstawkę. Wcześniej umieściłem w nim kilka milionów płyt i jest to bardzo szybkie. To powiedziawszy, chyba że będziesz musiał ponownie uruchomić tę wstawkę, może być łatwiej po prostu użyć EF.
Neil,
49

Zgadzam się z Adamem Rackisem. SqlBulkCopyto najszybszy sposób przesyłania zbiorczych rekordów z jednego źródła danych do drugiego. Użyłem tego do skopiowania rekordów 20 000 i zajęło to mniej niż 3 sekundy. Spójrz na poniższy przykład.

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

    }
}
Irfons
źródło
1
Wypróbowałem wiele rozwiązań przedstawionych w tym poście i SqlBulkCopy był zdecydowanie najszybszy. Czysty EF zajął 15 minut, ale dzięki mieszance roztworu i SqlBulkCopy udało mi się zejść do 1,5 minuty! To było z 2 milionami rekordów! Bez optymalizacji indeksu DB.
jonas
Lista jest łatwiejsza niż DataTable. Istnieje AsDataReader()metoda rozszerzenia, wyjaśniona w tej odpowiedzi: stackoverflow.com/a/36817205/1507899
RJB
Ale jest tylko dla najwyższej Istoty nie relacyjnej
Zahid Mustafa
1
@ZahidMustafa: tak. Robi to BulkInsert, a nie Bulk-Analysis-And-Relation-Trace-On-Object-Graphs. Jeśli chcesz objąć relacje, musisz przeanalizować i określić kolejność wstawiania, a następnie zbiorczo wstawić poszczególne poziomy i być może zaktualizować niektóre klucze jako potrzebne, a otrzymasz szybkie niestandardowe rozwiązanie. Możesz też polegać na EF, bez pracy po twojej stronie, ale wolniej w czasie wykonywania.
quetzalcoatl
23

Poleciłbym ten artykuł, w jaki sposób robić wkładki zbiorcze za pomocą EF.

Entity Framework i powolne WSTAWKI luzem

Bada te obszary i porównuje występy:

  1. Domyślny EF (57 minut, aby zakończyć dodawanie 30 000 rekordów)
  2. Zastąpienie kodem ADO.NET (25 sekund dla tych samych 30 000)
  3. Nadmuchanie kontekstu - Zachowaj mały aktywny wykres kontekstu, używając nowego kontekstu dla każdej jednostki pracy (te same 30 000 wstawek zajmuje 33 sekundy)
  4. Duże listy - wyłącz AutoDetectChangesEnabled (skraca czas do około 20 sekund)
  5. Dozowanie (do 16 sekund)
  6. DbTable.AddRange () - (wydajność mieści się w zakresie 12)
ShaTin
źródło
21

jak nigdy tu nie wspomniano, chcę polecić EFCore.BulkExtensions tutaj

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);
Manfred Wippel
źródło
1
Popieram tę sugestię. Po wypróbowaniu wielu rozwiązań homebrew skróciło to moją wkładkę do 1 sekundy z ponad 50 sekund. Jest to licencja MIT, którą można łatwo wdrożyć.
SouthShoreAK
jest to dostępne dla ef 6.x
Alok
jest to tylko bardziej wydajne niż użycie AddRange, jeśli ma ponad 10 jednostek
Szakal
5
10 000 wstawek zmieniło się z 9 minut na 12 sekund. To zasługuje na większą uwagę!
callisto
2
Jeśli istnieje jakikolwiek sposób na zmianę zaakceptowanych odpowiedzi, powinna to być teraz nowoczesna akceptowana odpowiedź. I chciałbym, żeby zespół EF dostarczył to po wyjęciu z pudełka.
Tanveer Badar
18

Zbadałem odpowiedź Slaumy (co jest niesamowite, dzięki za pomysłowca) i zmniejszałem wielkość partii, aż osiągną optymalną prędkość. Patrząc na wyniki Slauma:

  • commitCount = 1, createateContext = true: ponad 10 minut
  • commitCount = 10, createateContext = true: 241 sec
  • commitCount = 100, createateContext = true: 164 sec
  • commitCount = 1000, createateContext = true: 191 sec

Widoczne jest zwiększenie prędkości przy przechodzeniu od 1 do 10 i od 10 do 100, ale ze 100 do 1000 prędkość wstawiania ponownie spada.

Więc skupiłem się na tym, co się dzieje, gdy zmniejszasz wielkość partii do wartości między 10 a 100, a oto moje wyniki (używam innej zawartości wiersza, więc moje czasy mają inną wartość):

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

W oparciu o moje wyniki, rzeczywiste optimum wynosi około 30 dla wielkości partii. Jest mniej niż 10 i 100. Problem w tym, że nie mam pojęcia, dlaczego 30 jest optymalne, ani nie mogłem znaleźć żadnego logicznego wyjaśnienia tego.

Admir Tuzović
źródło
2
Znalazłem to samo w Postrges i czystym SQL (to zależy od SQL nie od EF), że 30 jest optymalne.
Kamil Gareev
Z mojego doświadczenia wynika, że ​​optymalne parametry różnią się w zależności od prędkości połączenia i wielkości rzędu. Dla szybkiego połączenia i małych rzędów optymalne może być nawet> 200 rzędów.
jing
18

Jak powiedzieli inni, SqlBulkCopy to sposób na zrobienie tego, jeśli chcesz naprawdę dobrej wydajności wstawiania.

Jest to trochę kłopotliwe we wdrożeniu, ale istnieją biblioteki, które mogą ci w tym pomóc. Jest ich kilka, ale tym razem bezwstydnie podłączę własną bibliotekę: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

Jedyny potrzebny kod to:

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

Ile to jest szybsze? Bardzo trudno powiedzieć, ponieważ zależy to od wielu czynników, wydajności komputera, sieci, wielkości obiektu itp. Testy wydajności, które przeprowadziłem, sugerują, że 25 000 jednostek można wstawić około 10s standardowego sposobu na localhost JEŻELI zoptymalizujesz konfigurację EF, np. wspomniane w innych odpowiedziach. Z EFUtilities, które zajmuje około 300ms. Jeszcze bardziej interesujące jest to, że dzięki tej metodzie zaoszczędziłem około 3 milionów jednostek w ciągu 15 sekund, średnio około 200 000 jednostek na sekundę.

Jedynym problemem jest oczywiście konieczność wstawienia powiązanych danych. Można to zrobić skutecznie na serwerze SQL za pomocą powyższej metody, ale wymaga strategii generowania Id, która pozwala generować identyfikatory w kodzie aplikacji dla nadrzędnego, dzięki czemu można ustawić klucze obce. Można to zrobić za pomocą identyfikatorów GUID lub czegoś takiego jak generowanie identyfikatora HiLo.

Mikael Eliasson
źródło
Działa dobrze. Składnia jest jednak nieco szczegółowa. Pomyśl, że byłoby lepiej, gdybyś EFBatchOperationmiał konstruktora, do którego przekazujesz, DbContextzamiast przechodzenia do każdej metody statycznej. Generyczne wersje InsertAlli UpdateAllautomatycznie znaleźć kolekcję, podobnie jak DbContext.Set<T>byłoby dobre.
kjbartel
Szybki komentarz do podziękowania! Ten kod pozwolił mi zapisać 170 000 rekordów w 1,5 sekundy! Całkowicie zdmuchuje każdą inną metodę, którą wypróbowałem z wody.
Tom Glenn
@Mikael Jeden problem dotyczy pól tożsamości. Czy masz już sposób na włączenie wstawiania tożsamości?
Joe Phillips
1
W przeciwieństwie do EntityFramework.BulkInsert ta biblioteka pozostała wolna. +1
Rudey,
14

Dispose()Kontekst stwarza problemy, jeśli elementy Add()polegają na innych wstępnie załadowanych elementach (np. właściwościach nawigacji) w kontekście

Używam podobnej koncepcji, aby mój kontekst był mały, aby osiągnąć taką samą wydajność

Ale zamiast Dispose()kontekstu i odtworzyć, po prostu odłączam jednostki, które już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;
} }

owiń go za pomocą try catch, a TrasactionScope()jeśli nie potrzebujesz, nie pokazuj go tutaj w celu utrzymania kodu w czystości

Stephen Ho
źródło
1
Spowolniło to wstawianie (AddRange) za pomocą Entity Framework 6.0. Wstawianie 20 000 wierszy wzrosło z około 101 sekund do 118 sekund.
AH.
1
@Stephen Ho: Staram się również unikać pozbywania się kontekstu. Rozumiem, że jest to wolniejsze niż odtwarzanie kontekstu, ale chcę wiedzieć, czy znalazłeś to wystarczająco szybko, niż nie odtwarzając kontekstu, ale z ustawionym zestawem zatwierdzeń.
Uczeń,
@Learner: Myślę, że było to szybsze niż odtworzenie kontekstu. Ale tak naprawdę nie pamiętam, bo w końcu przełączyłem się na SqlBulkCopy.
Stephen Ho
Skończyło się na tym, że użyłem tej techniki, ponieważ z jakiegoś dziwnego powodu pozostało trochę śladu po drugim przejściu przez pętlę while, mimo że miałem wszystko owinięte w instrukcję using i nawet wywołałem Dispose () w DbContext . Kiedy dodawałbym do kontekstu (przy drugim przejściu) liczba zestawów kontekstowych skoczyłaby do 6 zamiast tylko jednego. Inne elementy, które zostały arbitralnie dodane, zostały już wstawione w pierwszym przejściu przez pętlę while, więc wywołanie SaveChanges nie powiedzie się przy drugim przejściu (z oczywistych powodów).
Hallmanac
9

Wiem, że to bardzo stare pytanie, ale jeden facet powiedział, że opracował metodę rozszerzenia do użycia wstawiania zbiorczego z EF, a kiedy sprawdziłem, odkryłem, że biblioteka kosztuje dziś 599 USD (dla jednego programisty). Może ma to sens dla całej biblioteki, jednak dla samej wstawki zbiorczej jest to zbyt wiele.

Oto bardzo prosta metoda rozszerzenia, którą stworzyłem. Najpierw używam tego w parze z bazą danych (najpierw nie testuj z kodem, ale myślę, że to działa tak samo). Zmień YourEntitiesz nazwą swojego kontekstu:

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

Możesz użyć tego przeciwko dowolnej kolekcji, która dziedziczy IEnumerable, w następujący sposób:

await context.BulkInsertAllAsync(items);
Guilherme
źródło
uzupełnij swój przykładowy kod. where is bulkCopy
Seabizkit
1
Jest już tutaj:await bulkCopy.WriteToServerAsync(table);
Guilherme,
Być może nie było jasne, że w swoim piśmie sugerujesz, abyś zrobił rozszerzenie ... co wziąłem na myśli, że nie jest potrzebna żadna część trzeciej biblioteki lib, podczas gdy w rzeczywistości w obu metodach używasz biblioteki SqlBulkCopy lib. To całkowicie zależy od SqlBulkCopy, kiedy zapytałem, skąd pochodzi bulkCopy, jest to biblioteka rozszerzeń, na której napisałeś lib rozszerzenia. Bardziej sensownym byłoby powiedzenie tutaj, jak korzystałem z SqlBulkCopy lib.
Seabizkit
powinien użyć conn.OpenAsync w wersji asynchronicznej
Robert
6

Spróbuj użyć procedury składowanej , która otrzyma XML danych, które chcesz wstawić.

Maksyma
źródło
9
Przekazywanie danych jako XML nie jest potrzebne, jeśli nie chcesz przechowywać ich jako XML. W SQL 2008 można użyć parametru wycenionego w tabeli.
Ladislav Mrnka
Nie wyjaśniłem tego, ale muszę również obsługiwać SQL 2005
Bongo Sharp
4

Zrobiłem ogólne rozszerzenie powyższego przykładu @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;
    }
}

Stosowanie:

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();
    }
}
Sgedda
źródło
4

Szukam najszybszego sposobu wstawienia do Entity Framework

Dostępne są biblioteki innych firm obsługujące wstawianie luzem:

  • Z.EntityFramework.Extensions ( zalecane )
  • EFUtilities
  • EntityFramework.BulkInsert

Zobacz: Biblioteka Entity Framework Bulk Insert

Zachowaj ostrożność przy wyborze biblioteki wstawiania zbiorczego. Tylko rozszerzenia Entity Framework obsługują wszelkiego rodzaju powiązania i dziedziczenia i jest to jedyne obsługiwane.


Oświadczenie : Jestem właścicielem rozszerzeń Entity Framework

Ta biblioteka pozwala na wykonywanie wszystkich operacji masowych potrzebnych w Twoich scenariuszach:

  • Bulk SaveChanges
  • Wstaw luzem
  • Usuń zbiorczo
  • Aktualizacja zbiorcza
  • Łączenie zbiorcze

Przykład

// 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;
});
Jonathan Magnan
źródło
19
jest to świetne rozszerzenie, ale nie darmowe .
Okan Kocyigit
2
Ta odpowiedź jest całkiem dobra, a EntityFramework.BulkInsert wykonuje zbiorcze wstawianie 15 000 wierszy w 1,5 sekundy, działa całkiem nieźle dla wewnętrznego procesu takiego jak usługa Windows.
Pastor Cortes
4
Tak, 600 $ za wkładkę luzem. Całkowicie tego warte.
eocron,
1
@eocron Tak, warto, jeśli używasz go komercyjnie. Nie widzę problemu z 600 USD za coś, czego sam nie muszę spędzać godzinami, co kosztuje mnie znacznie więcej niż 600 USD. Tak, to kosztuje, ale patrząc na moją stawkę godzinową, to dobrze wydane pieniądze!
Jordy van Eijk
3

Użyj 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;
}
Amir Saniyan
źródło
3

Jednym z najszybszych sposobów zapisania listy jest zastosowanie następującego kodu

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

AutoDetectChangesEnabled = false

Add, AddRange i SaveChanges: Nie wykrywa zmian.

ValidateOnSaveEnabled = false;

Nie wykrywa modułu do śledzenia zmian

Musisz dodać nuget

Install-Package Z.EntityFramework.Extensions

Teraz możesz użyć następującego kodu

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();
Reza Jenabi
źródło
czy mogę użyć Twojego przykładowego kodu do zbiorczej aktualizacji?
AminGolmahalle
4
Biblioteka Z nie jest darmowa
SHADOW.NET
3

SqlBulkCopy jest bardzo szybki

To jest moja realizacja:

// 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;
        }
    }
}
Philip Johnson
źródło
3

[Aktualizacja 2019] EF Core 3.1

Zgodnie z tym, co powiedziano powyżej, wyłączenie AutoDetectChangesEnabled w EF Core działało idealnie: czas wstawienia został podzielony przez 100 (od wielu minut do kilku sekund, 10 000 rekordów z relacjami między tabelami)

Zaktualizowany kod to:

  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
XavierAM
źródło
2

Oto porównanie wydajności między użyciem Entity Framework a użyciem klasy SqlBulkCopy na realistycznym przykładzie: Jak luzem wstawiać złożone obiekty do bazy danych SQL Server

Jak już podkreślili inni, ORM nie są przeznaczone do użycia w operacjach masowych. Oferują elastyczność, rozdzielenie problemów i inne korzyści, ale operacje masowe (z wyjątkiem masowego odczytu) nie są jedną z nich.

Zoran Horvat
źródło
2

Inną opcją jest użycie SqlBulkTools dostępnych w Nuget. Jest bardzo łatwy w użyciu i ma kilka zaawansowanych funkcji.

Przykład:

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

Zobacz dokumentację, aby uzyskać więcej przykładów i zaawansowanych zastosowań. Oświadczenie: Jestem autorem tej biblioteki i wszelkie poglądy są moim zdaniem.

Greg R. Taylor
źródło
2
Ten projekt został usunięty zarówno z NuGet, jak i GitHub.
0xced
1

Zgodnie z moją wiedzą jest no BulkInsertw EntityFrameworkcelu zwiększenia wydajności ogromnych wkładek.

W tym scenariuszu możesz przejść do SqlBulkCopy w, ADO.netaby rozwiązać problem

anishMarokey
źródło
Patrzyłem na tę klasę, ale wydaje się, że jest bardziej zorientowana na wstawianie od stołu do stołu, prawda?
Bongo Sharp
Nie jestem pewien, co masz na myśli, ma przeciążenie, WriteToServerktóre zajmuje DataTable.
Blindy,
nie, możesz także wstawiać obiekty SQL. SQL do SQL. Czego szukasz?
anishMarokey
Sposób na wstawienie potencjalnie tysięcy rekordów do bazy danych w bloku TransactionScope
Bongo Sharp
możesz użyć .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey
1

Czy próbowałeś kiedyś wstawić pracownika lub zadanie w tle?

W moim przypadku im wstawiam 7760 rejestrów, rozproszonych w 182 różnych tabelach z relacjami klucza obcego (według NavigationProperties).

Bez zadania zajęło to 2 i pół minuty. W ramach zadania ( Task.Factory.StartNew(...)) zajęło to 15 sekund.

Robię tylko SaveChanges()po dodaniu wszystkich bytów do kontekstu. (w celu zapewnienia integralności danych)

Rafael AMS
źródło
2
Jestem pewien, że kontekst nie jest bezpieczny dla wątków. Czy masz testy, aby upewnić się, że wszystkie byty zostały zapisane?
Danny Varod
Wiem, że cała struktura encji wcale nie jest bezpieczna dla wątków, ale po prostu dodaję obiekty do kontekstu i zapisuję na końcu ... Działa tutaj idealnie.
Rafael AMS
Wywołujesz więc DbContext.SaveChanges () w głównym wątku, ale dodawanie encji do kontekstu odbywa się w wątku w tle, prawda?
Prokurors
1
Tak, dodaj dane do wątków; poczekaj, aż wszyscy skończą; i Zapisz zmiany w głównym wątku
Rafael AMS
Chociaż uważam, że ta droga jest niebezpieczna i podatna na błędy, uważam ją za bardzo interesującą.
Uczeń,
1

Wszystkie opisane tutaj rozwiązania nie pomagają, ponieważ kiedy wykonujesz SaveChanges (), instrukcje insert są wysyłane do bazy danych jedna po drugiej, tak działa Entity.

A jeśli na przykład podróż do bazy danych iz powrotem trwa 50 ms, czas potrzebny na wstawienie to liczba rekordów x 50 ms.

Musisz użyć BulkInsert, oto link: https://efbulkinsert.codeplex.com/

Dzięki temu czas wstawiania został skrócony z 5-6 minut do 10-12 sekund.

Aleksa
źródło
1

[NOWE ROZWIĄZANIE DLA POSTGRESQL] Hej, wiem, że to dość stary post, ale ostatnio miałem podobny problem, ale korzystaliśmy z Postgresql. Chciałem zastosować skuteczny bulkinsert, co okazało się dość trudne. Nie znalazłem żadnej odpowiedniej darmowej biblioteki, aby to zrobić na tym DB. Znalazłem tylko tego pomocnika: https://bytefish.de/blog/postgresql_bulk_insert/, który jest również dostępny w Nuget. Napisałem małego mapera, który automatycznie mapuje właściwości w sposób 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;
        }

Używam go w następujący sposób (miałem podmiot o nazwie Zobowiązanie):

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

Pokazałem przykład transakcji, ale można to również zrobić przy normalnym połączeniu pobranym z kontekstu. enterpriseToAdd jest wyliczalny dla normalnych rekordów encji, które chcę zbiorczo wstawić do DB.

To rozwiązanie, do którego mam po kilku godzinach badań i prób, jest, jak można się spodziewać, znacznie szybsze i wreszcie łatwe w użyciu i darmowe! Naprawdę radzę korzystać z tego rozwiązania, nie tylko z wyżej wymienionych powodów, ale także dlatego, że jest to jedyne, z którym nie miałem problemów z samym Postgresqlem, wiele innych rozwiązań działa bezbłędnie, na przykład z SqlServer.

Michał Pilarek
źródło
0

Sekret polega na wstawieniu do identycznej pustej tabeli pomostowej. Wkładki błyskawicznie się rozjaśniają. Następnie uruchom jedną wstawkę z tego do głównego dużego stołu. Następnie obetnij stół pomostowy gotowy do następnej partii.

to znaczy.

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
Simon Hughes
źródło
Za pomocą EF dodaj wszystkie swoje rekordy do pustej tabeli pomostowej. Następnie użyj SQL, aby wstawić do głównej (dużej i wolnej) tabeli w pojedynczej instrukcji SQL. Następnie opróżnij swój stół przeciwstawny. Jest to bardzo szybki sposób wstawiania dużej ilości danych do już dużej tabeli.
Simon Hughes
13
Kiedy mówisz, używając EF, dodaj rekordy do tabeli pomostowej, czy faktycznie próbowałeś tego z EF? Ponieważ EF wydaje osobne wywołanie do bazy danych z każdą wstawką, podejrzewam, że zobaczysz to samo trafienie, którego OP próbuje uniknąć. W jaki sposób tabela pomostowa unika tego problemu?
Jim Wooley,
-1

Ale dla więcej niż (+4000) wkładek zalecam stosowanie procedury składowanej. dołączony czas, który upłynął. Wstawiłem 11,788 wierszy w 20 "wprowadź opis zdjęcia tutaj

to jest kod

 public void InsertDataBase(MyEntity entity)
    {
        repository.Database.ExecuteSqlCommand("sp_mystored " +
                "@param1, @param2"
                 new SqlParameter("@param1", entity.property1),
                 new SqlParameter("@param2", entity.property2));
    }
Marinpietri
źródło
-1

Użyj procedury składowanej, która pobiera dane wejściowe w postaci xml.

Z kodu c # pass wstaw dane jako xml.

np. w c #, składnia wyglądałaby tak:

object id_application = db.ExecuteScalar("procSaveApplication", xml)
Arun Tiwari
źródło
-7

Użyj tej techniki, aby zwiększyć szybkość wstawiania rekordów w Entity Framework. Tutaj używam prostej procedury składowanej, aby wstawić rekordy. I do wykonania tej procedury składowanej używam metody .FromSql () Entity Framework, która wykonuje Raw SQL.

Kod procedury składowanej:

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

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

Następnie przejrzyj wszystkie 4000 rekordów i dodaj kod Entity Framework, który wykonuje zapisany zapis

procedura wykonuje się co 100 pętlę.

W tym celu tworzę zapytanie łańcuchowe w celu wykonania tej procedury, ciągle dołączam do niego każdy zestaw rekordów.

Następnie sprawdź, czy pętla działa w wielokrotnościach 100 i w takim przypadku uruchom ją za pomocą .FromSql().

Tak więc dla 4000 rekordów muszę wykonać procedurę tylko 4000/100 = 40 razy .

Sprawdź poniższy kod:

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 = "";
    }
}
Mallory H.
źródło
Może to być wydajne, ale równoważne z NIE korzystaniem ze struktury encji. Pytanie PO brzmiało: jak zmaksymalizować wydajność w kontekście Entity Framework
kall2sollies,