Modo più rapido di inserimento in Entity Framework


682

Sto cercando il modo più veloce per inserire in Entity Framework.

Lo sto chiedendo a causa dello scenario in cui hai un TransactionScope attivo e l'inserimento è enorme (4000+). Può durare potenzialmente più di 10 minuti (timeout predefinito delle transazioni) e ciò comporterà una transazione incompleta.


1
Come lo stai facendo attualmente?
Dustin Laine,

Creazione di TransactionScope, creazione di un'istanza di DBContext, apertura della connessione e in un'istruzione for-each che esegue gli inserimenti e SavingChanges (per ogni record), NOTA: TransactionScope e DBContext sono in istruzioni usando e sto chiudendo la connessione in un blocco
Bongo Sharp

Un'altra risposta per riferimento: stackoverflow.com/questions/5798646/...
Ladislav Mrnka

2
Il modo più rapido di inserimento in un database SQL non prevede EF. AFAIK Il suo BCP quindi TVP + Unisci / Inserisci.
StingyJack

1
Per coloro che leggeranno i commenti: la risposta più applicabile e moderna è qui.
Tanveer Badar,

Risposte:


986

Alla tua osservazione nei commenti alla tua domanda:

"... SavingChanges ( per ogni record ) ..."

Questa è la cosa peggiore che puoi fare! La chiamata SaveChanges()per ogni record rallenta notevolmente gli inserti di massa. Farei alcuni semplici test che molto probabilmente miglioreranno le prestazioni:

  • Chiama SaveChanges()una volta dopo TUTTI i record.
  • Chiamare SaveChanges()dopo ad esempio 100 record.
  • Chiamare SaveChanges()dopo ad esempio 100 record e disporre il contesto e crearne uno nuovo.
  • Disabilita il rilevamento delle modifiche

Per inserti di massa sto lavorando e sperimentando un modello come questo:

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

Ho un programma di test che inserisce 560.000 entità (9 proprietà scalari, nessuna proprietà di navigazione) nel DB. Con questo codice funziona in meno di 3 minuti.

Per le prestazioni è importante chiamare SaveChanges()dopo "molti" record ("molti" circa 100 o 1000). Migliora anche le prestazioni per disporre il contesto dopo SaveChanges e crearne uno nuovo. Questo cancella il contesto da tutte le entità, SaveChangesnon lo fa, le entità sono ancora attaccate al contesto nello stato Unchanged. È la dimensione crescente delle entità collegate nel contesto che rallenta l'inserimento passo dopo passo. Quindi, è utile cancellarlo dopo qualche tempo.

Ecco alcune misure per le mie 560000 entità:

  • commitCount = 1 ,reatteContext = false: molte ore (Questa è la procedura corrente)
  • commitCount = 100, ricreateContext = false: più di 20 minuti
  • commitCount = 1000, ricreateContext = false: 242 sec
  • commitCount = 10000, ricreateContext = false: 202 sec
  • commitCount = 100000, ricreateContext = false: 199 sec
  • commitCount = 1000000 ,reatteContext = false: eccezione di memoria insufficiente
  • commitCount = 1, ricreateContext = true: più di 10 minuti
  • commitCount = 10, ricreateContext = true: 241 sec
  • commitCount = 100, ricreateContext = true: 164 sec
  • commitCount = 1000, ricreateContext = true: 191 sec

Il comportamento nel primo test sopra è che le prestazioni sono molto non lineari e diminuiscono estremamente nel tempo. ("Molte ore" è una stima, non ho mai finito questo test, mi sono fermato a 50.000 entità dopo 20 minuti.) Questo comportamento non lineare non è così significativo in tutti gli altri test.


89
@Bongo Sharp: non dimenticare di impostare AutoDetectChangesEnabled = false;su DbContext. Essa ha anche un grande effetto prestazioni aggiuntive: stackoverflow.com/questions/5943394/...
Slauma

6
Sì, il problema è che sto usando Entity Framework 4 e AutoDetectChangesEnabled fa parte della 4.1, tuttavia, ho fatto il test delle prestazioni e ho avuto RISULTATI INCREDIBILI, è passato dalle 00:12:00 alle 00:00:22 SavinChanges su ogni entità stava facendo l'olverload ... GRAZIE mille per la tua risposta! questo è quello che stavo cercando
Bongo Sharp,

10
Grazie per il contesto.Configuration.AutoDetectChangesEnabled = false; suggerimento, fa una differenza enorme .
douglaz,

1
@ dahacker89: stai usando la versione corretta EF> = 4.1 e DbContext, NON ObjectContext?
Slauma,

3
@ dahacker89: ti suggerisco di creare una domanda separata per il tuo problema con forse maggiori dettagli. Non sono in grado di capire qui cosa c'è che non va.
Slauma,

176

Questa combinazione aumenta la velocità abbastanza bene.

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

46
Non disabilitare ciecamente ValidateOnSaveEnabled, potresti dipendere da quel comportamento e non realizzarlo fino a quando non è troppo tardi. Quindi, potresti eseguire la convalida altrove nel codice e avere la convalida EF ancora una volta non è del tutto necessario.
Jeremy Cook,

1
Nel mio test il salvataggio di 20.000 file è passato da 101 secondi a 88 secondi. Non molto e quali sono le implicazioni.
AH.

27
@JeremyCook Penso che ciò che stai cercando di ottenere sia che questa risposta sarebbe molto migliore se spiegasse le possibili implicazioni della modifica di queste proprietà dai loro valori predefiniti (a parte il miglioramento delle prestazioni). Sono d'accordo.
pseudocoder

1
Questo ha funzionato per me, anche se se stai aggiornando i record nel contesto dovrai chiamare esplicitamente DetectChanges ()
hillstuk

2
Questi possono essere disattivato e quindi riattivato con un try-finally: msdn.microsoft.com/en-us/data/jj556205.aspx
yellavon

98

Il modo più veloce sarebbe usare l' estensione dell'inserto di massa , che ho sviluppato

nota: questo è un prodotto commerciale, non gratuito

Utilizza SqlBulkCopy e Datareader personalizzato per ottenere le massime prestazioni. Di conseguenza è oltre 20 volte più veloce rispetto all'utilizzo dell'inserto regolare o AddRange EntityFramework.BulkInsert vs EF AddRange

l'utilizzo è estremamente semplice

context.BulkInsert(hugeAmountOfEntities);

10
Veloce ma solo il livello superiore di una gerarchia.
Bloke CAD,

66
Non è gratuito
Amir Saniyan il

72
Gli annunci stanno diventando più intelligenti ... questo è un prodotto a pagamento e molto costoso per un libero professionista. Stai attento!
JulioQc,

35
600 USD per supporto e aggiornamenti di 1 anno? Siete fuori di testa?
Camilo Terevinto,

7
non sono più il proprietario del prodotto
maxlego

83

Dovresti cercare di usare il System.Data.SqlClient.SqlBulkCopyper questo. Ecco la documentazione e, naturalmente, ci sono molti tutorial online.

Siamo spiacenti, so che stavi cercando una risposta semplice per fare in modo che EF facesse quello che volevi, ma le operazioni in blocco non sono realmente ciò per cui sono destinati gli ORM.


1
Mi sono imbattuto in SqlBulkCopy un paio di volte durante la ricerca di questo, ma sembra essere più orientato agli inserti da tavolo a tavolo, purtroppo non mi aspettavo soluzioni facili, ma piuttosto suggerimenti sulle prestazioni, come ad esempio la gestione dello stato del connessione manuale, istinto di lasciare che EF lo faccia per te
Bongo Sharp

7
Ho usato SqlBulkCopy per inserire grandi quantità di dati direttamente dalla mia applicazione. Che, fondamentalmente, è necessario creare un DataTable, riempirlo, quindi passare che a BulkCopy. Ci sono alcuni trucchi mentre stai configurando la tua DataTable (la maggior parte della quale ho dimenticato, purtroppo), ma dovrebbe funzionare bene
Adam Rackis,

2
Ho fatto la prova del concetto e, come promesso, funziona molto velocemente, ma uno dei motivi per cui sto usando EF è perché l'inserimento di dati relazionali è più semplice, ad esempio se inserisco un'entità che contiene già dati relazionali , lo inserirà anche, sei mai entrato in questo scenario? Grazie!
Bongo Sharp,

2
Sfortunatamente, l'inserimento di una ragnatela di oggetti in un DBMS non è realmente qualcosa che BulkCopy farà. Questo è il vantaggio di un ORM come EF, il cui costo è che non si ridimensionerà per fare centinaia di grafici a oggetti simili in modo efficiente.
Adam Rackis,

2
SqlBulkCopy è sicuramente la strada da percorrere se hai bisogno di velocità raw o se eseguirai di nuovo questo inserto. Ho inserito diversi milioni di dischi con esso prima ed è estremamente veloce. Detto questo, a meno che non sia necessario rieseguire questo inserto, potrebbe essere più semplice utilizzare solo EF.
Neil,

49

Sono d'accordo con Adam Rackis. SqlBulkCopyè il modo più veloce di trasferire record di massa da un'origine dati a un'altra. Ho usato questo per copiare 20K record e ci sono voluti meno di 3 secondi. Dai un'occhiata all'esempio qui sotto.

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

    }
}

1
Ho provato molte delle soluzioni fornite in questo post e SqlBulkCopy è stato di gran lunga il più veloce. Pure EF ha impiegato 15 minuti, ma con un mix di soluzione e SqlBulkCopy sono riuscito a scendere a 1,5 minuti! Questo è stato con 2 milioni di record! Senza alcuna ottimizzazione dell'indice DB.
jonas,

L'elenco è più semplice di DataTable. C'è un AsDataReader()metodo di estensione, spiegato in questa risposta: stackoverflow.com/a/36817205/1507899
RJB

Ma è solo per l'Entità superiore non relazionale
Zahid Mustafa,

1
@ZahidMustafa: sì. Sta facendo BulkInsert, non Bulk-Analysis-And-Relation-Tracing-On-Object-Graphs .. se vuoi coprire le relazioni, devi analizzare e determinare l'ordine di inserimento e quindi inserire in blocco i singoli livelli e forse aggiornare alcune chiavi come necessario e otterrai una rapida soluzione personalizzata. Oppure, puoi fare affidamento su EF per farlo, nessun lavoro dalla tua parte, ma più lento in fase di esecuzione.
quetzalcoatl,

23

Consiglierei questo articolo su come eseguire inserti di massa usando EF.

Entity Framework e INSERT lenti alla rinfusa

Esplora queste aree e confronta le prestazioni:

  1. EF predefinito (57 minuti per completare l'aggiunta di 30.000 record)
  2. Sostituzione con codice ADO.NET (25 secondi per gli stessi 30.000)
  3. Contesto gonfio: mantieni piccolo il grafico di contesto attivo utilizzando un nuovo contesto per ogni unità di lavoro (gli stessi 30.000 inserti richiedono 33 secondi)
  4. Elenchi di grandi dimensioni: disattiva AutoDetectChangesEnabled (riduce il tempo a circa 20 secondi)
  5. Batching (fino a 16 secondi)
  6. DbTable.AddRange () - (le prestazioni sono nell'intervallo 12)

21

come non è mai stato menzionato qui, voglio consigliare EFCore.BulkExtensions qui

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

1
Secondo questo suggerimento. Dopo aver provato molte soluzioni homebrew questo ha ridotto il mio inserto di 1 secondo da oltre 50 secondi. Ed è la licenza MIT così facile da incorporare.
SouthShoreAK

è disponibile per ef 6.x
Alok

questo è solo più performante dell'utilizzo di AddRange se è superiore a 10 entità
Jackal

5
10.000 inserti sono passati da 9 minuti a 12 secondi. Questo merita più attenzione!
callisto,

2
Se c'è un modo per cambiare le risposte accettate, questa dovrebbe essere la moderna risposta accettata ora. E vorrei che il team EF lo avesse fornito immediatamente.
Tanveer Badar,

18

Ho studiato la risposta di Slauma (che è fantastico, grazie per l'idea uomo) e ho ridotto le dimensioni del lotto fino a raggiungere la velocità ottimale. Guardando i risultati di Slauma:

  • commitCount = 1, ricreateContext = true: più di 10 minuti
  • commitCount = 10, ricreateContext = true: 241 sec
  • commitCount = 100, ricreateContext = true: 164 sec
  • commitCount = 1000, ricreateContext = true: 191 sec

È visibile un aumento della velocità quando si passa da 1 a 10 e da 10 a 100, ma da 100 a 1000 la velocità di inserimento diminuisce nuovamente.

Quindi mi sono concentrato su ciò che sta accadendo quando riduci le dimensioni del batch per un valore compreso tra 10 e 100, e qui ci sono i miei risultati (sto usando diversi contenuti di riga, quindi i miei tempi hanno un valore diverso):

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

Sulla base dei miei risultati, l'ottimale effettivo è di circa 30 per la dimensione del lotto. È inferiore sia a 10 che a 100. Il problema è che non ho idea del perché sia ​​ottimale 30, né avrei potuto trovare alcuna spiegazione logica per questo.


2
Ho trovato lo stesso con Postrges e SQL puro (dipende da SQL, non da EF) che 30 è ottimale.
Kamil Gareev,

La mia esperienza è che l'ottimale differisce per velocità di connessione e dimensioni della riga diverse. Per una connessione veloce e file di dimensioni ridotte, l'ottimale può essere anche> 200 file.
jing

18

Come altri hanno già detto, SqlBulkCopy è il modo di farlo se si desidera ottenere prestazioni di inserimento davvero buone.

È un po 'complicato da implementare ma ci sono librerie che possono aiutarti. Ce ne sono alcuni là fuori, ma stavolta collegherò senza vergogna la mia libreria: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

L'unico codice di cui hai bisogno è:

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

Quindi quanto è più veloce? Molto difficile da dire perché dipende da tanti fattori, prestazioni del computer, rete, dimensioni degli oggetti, ecc. I test delle prestazioni che ho fatto suggeriscono che 25k entità possono essere inserite a circa 10 secondi nello standard host locale se ottimizzi la tua configurazione EF come menzionato nelle altre risposte. Con EFUtilities che impiega circa 300ms. Ancora più interessante è che ho salvato circa 3 milioni di entità in meno di 15 secondi usando questo metodo, con una media di circa 200k entità al secondo.

L'unico problema è ovvio se è necessario inserire dati ripetuti. Questo può essere fatto efficacemente in SQL Server usando il metodo sopra ma richiede che tu abbia una strategia di generazione di Id che ti consenta di generare ID nel codice app per il genitore in modo da poter impostare le chiavi esterne. Questo può essere fatto usando GUID o qualcosa come la generazione dell'ID HiLo.


Funziona bene. La sintassi è tuttavia un po 'dettagliata. Pensa che sarebbe meglio se EFBatchOperationavesse un costruttore a cui passi DbContextinvece di passare a ogni metodo statico. Anche le versioni generiche InsertAlle UpdateAllche trovano automaticamente la raccolta, simili a DbContext.Set<T>, sarebbero buone.
kjbartel,

Solo un breve commento per dire grazie! Questo codice mi ha permesso di salvare 170.000 record in 1,5 secondi! Soffia completamente qualsiasi altro metodo che ho provato fuori dall'acqua.
Tom Glenn,

@Mikael Un problema riguarda i campi di identità. Hai ancora un modo per abilitare l'inserimento dell'identità?
Joe Phillips,

1
Contrariamente a EntityFramework.BulkInsert, questa libreria è rimasta libera. +1
Rudey,

14

Dispose()il contesto crea problemi se le entità su cui si fa Add()affidamento su altre entità precaricate (ad es. proprietà di navigazione) nel contesto

Uso un concetto simile per mantenere piccolo il mio contesto per ottenere le stesse prestazioni

Ma invece del Dispose()contesto e ricreare, semplicemente distacco le entità che già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;
} }

avvolgetelo con try catch e, TrasactionScope()se necessario, non mostratelo qui per mantenere pulito il codice


1
Ciò ha rallentato l'inserto (AddRange) utilizzando Entity Framework 6.0. L'inserimento di 20.000 righe è aumentato da circa 101 secondi a 118 secondi.
AH.

1
@Stephen Ho: Sto anche cercando di evitare di disporre il mio contesto. Posso capire che è più lento che ricreare il contesto, ma voglio sapere se lo hai trovato abbastanza velocemente da non ricreare il contesto ma con un set commitCount.
Studente il

@Learner: penso che sia stato più veloce che ricreare il contesto. Ma non ricordo davvero ora perché sono finalmente passato a usare SqlBulkCopy.
Stephen Ho,

Ho finito per usare questa tecnica perché, per qualche strana ragione, c'era un po 'di tracciamento rimasto sul secondo passaggio attraverso il ciclo while, anche se avevo tutto racchiuso in un'istruzione using e persino chiamato Dispose () su DbContext . Quando aggiungerei al contesto (al secondo passaggio) il conteggio del set di contesti salta a 6 anziché a uno solo. Gli altri elementi che sono stati aggiunti in modo arbitrario erano già stati inseriti nel primo passaggio del ciclo while in modo che la chiamata a SaveChanges non riuscisse nel secondo passaggio (per ovvi motivi).
Hallmanac,

9

So che questa è una domanda molto vecchia, ma un ragazzo qui ha detto che ha sviluppato un metodo di estensione per utilizzare l'inserimento di massa con EF, e quando ho controllato, ho scoperto che la libreria costa $ 599 oggi (per uno sviluppatore). Forse ha senso per l'intera libreria, tuttavia solo per l'inserimento di massa questo è troppo.

Ecco un metodo di estensione molto semplice che ho creato. Lo uso prima sull'associazione con il database (non testare prima con il codice, ma penso che funzioni allo stesso modo). Cambia YourEntitiescon il nome del tuo contesto:

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

Puoi usarlo contro qualsiasi raccolta da cui eredita IEnumerable, in questo modo:

await context.BulkInsertAllAsync(items);

per favore completa il tuo codice di esempio. dove si trova bulkCopy
Seabizkit

1
È già qui:await bulkCopy.WriteToServerAsync(table);
Guilherme,

Forse non ero chiaro, nel tuo scrivere, suggerisci di aver creato un'estensione ... che ho inteso nel senso che non era necessaria alcuna lib di terza parte, quando in effetti in entrambi i metodi usa la lib di SqlBulkCopy. Questo si basa interamente su SqlBulkCopy, quando ho chiesto da dove provenga bulkCopy, è una libreria di estensioni su cui hai scritto una libreria di estensioni. Avrebbe semplicemente avuto più senso dire qui è come ho usato la libreria SqlBulkCopy.
Seabizkit,

dovrebbe usare conn.OpenAsync in versione asincrona
Robert

6

Prova a utilizzare una Stored Procedure che otterrà un XML dei dati che desideri inserire.


9
Il passaggio di dati come XML non è necessario se non si desidera archiviarli come XML. In SQL 2008 è possibile utilizzare il parametro con valori di tabella.
Ladislav Mrnka,

non ho chiarito questo, ma devo supportare anche SQL 2005
Bongo Sharp,

4

Ho fatto un'estensione generica dell'esempio di @Slauma sopra;

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

Uso:

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

4

Sto cercando il modo più veloce per inserire in Entity Framework

Sono disponibili alcune librerie di terze parti che supportano l'inserimento di massa:

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

Vedi: Entity Framework Bulk Insert library

Fare attenzione, quando si sceglie una libreria di inserimento di massa. Solo Entity Framework Extensions supporta tutti i tipi di associazioni ed eredità ed è l'unico ancora supportato.


Disclaimer : sono il proprietario di Entity Framework Extensions

Questa libreria ti consente di eseguire tutte le operazioni in blocco necessarie per i tuoi scenari:

  • Bulk SaveChanges
  • Inserto sfuso
  • Elimina in blocco
  • Aggiornamento in blocco
  • Unione in blocco

Esempio

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

19
questa è una grande estensione ma non gratuita .
Okan Kocyigit,

2
Questa risposta è piuttosto buona ed EntityFramework.BulkInsert esegue un inserimento in blocco di 15K righe in 1,5 secondi, funziona piuttosto bene per un processo interno come un Servizio Windows.
Pastor Cortes,

4
Sì, 600 $ per inserto sfuso. Ne vale la pena.
eocron,

1
@eocron Yeat ne vale la pena se lo usi in modo commerciale. Non vedo alcun problema con $ 600 per qualcosa che non devo passare ore a costruirlo da solo, il che mi costerà molto più di $ 600. Sì, costa denaro ma guardando la mia tariffa oraria è denaro che spendi bene!
Jordy van Eijk,

3

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

3

Uno dei modi più veloci per salvare un elenco è necessario applicare il seguente codice

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

AutoDetectChangesEnabled = false

Aggiungi, Aggiungi intervallo e Salva modifiche: non rileva le modifiche.

ValidateOnSaveEnabled = false;

Non rileva il tracker modifiche

Devi aggiungere nuget

Install-Package Z.EntityFramework.Extensions

Ora puoi usare il seguente codice

var context = new MyContext();

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

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

posso usare il tuo codice di esempio per l'aggiornamento collettivo?
AminGolmahalle,

4
La libreria Z non è gratuita
SHADOW.NET

3

SqlBulkCopy è super veloce

Questa è la mia implementazione:

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

3

[Aggiornamento 2019] EF Core 3.1

In seguito a quanto detto sopra, la disabilitazione di AutoDetectChangesEnabled in EF Core ha funzionato perfettamente: il tempo di inserimento è stato diviso per 100 (da molti minuti a pochi secondi, 10k record con relazioni tra tabelle)

Il codice aggiornato è:

  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

2

Ecco un confronto delle prestazioni tra l'utilizzo di Entity Framework e l'uso della classe SqlBulkCopy su un esempio realistico: Come inserire in blocco oggetti complessi nel database di SQL Server

Come altri hanno già sottolineato, gli ORM non sono pensati per essere utilizzati in operazioni in blocco. Offrono flessibilità, separazione delle preoccupazioni e altri vantaggi, ma le operazioni in blocco (tranne la lettura in blocco) non sono tra queste.


2

Un'altra opzione è utilizzare SqlBulkTools disponibile da Nuget. È molto facile da usare e ha alcune potenti funzionalità.

Esempio:

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

Vedere la documentazione per ulteriori esempi e utilizzo avanzato. Dichiarazione di non responsabilità: sono l'autore di questa libreria e tutte le opinioni sono a mio avviso.


2
Questo progetto è stato eliminato da NuGet e GitHub.
0xced

1

Come per la mia conoscenza non v'è no BulkInsertin EntityFrameworkper aumentare le prestazioni dei grandi inserti.

In questo scenario si può andare con SqlBulkCopy in ADO.netper risolvere il problema


Stavo dando un'occhiata a quella classe, ma sembra essere più orientato agli inserimenti da tavolo a tavolo, no?
Bongo Sharp,

Non sono sicuro di cosa intendi, ha un sovraccarico WriteToServerche richiede a DataTable.
Blindy,

no puoi inserire anche oggetti .Net in SQL. Cosa stai cercando?
anishMarokey,

Un modo per inserire potenzialmente migliaia di record nel database all'interno di un blocco TransactionScope
Bongo Sharp

puoi utilizzare .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey

1

Hai mai provato a inserire un lavoratore o un'attività in background?

Nel mio caso, sto inserendo 7760 registri, distribuiti in 182 diverse tabelle con relazioni di chiave esterna (di NavigationProperties).

Senza il compito, ci sono voluti 2 minuti e mezzo. All'interno di un'attività ( Task.Factory.StartNew(...)), ci sono voluti 15 secondi.

Sto facendo solo SaveChanges()dopo aver aggiunto tutte le entità al contesto. (per garantire l'integrità dei dati)


2
Sono abbastanza sicuro che il contesto non sia thread-safe. Avete dei test per garantire che tutte le entità siano state salvate?
Danny Varod,

So che l'intero framework di entità non è affatto sicuro, ma sto solo aggiungendo gli oggetti al contesto e salvando alla fine ... Funziona perfettamente qui.
Rafael AMS,

Quindi, stai chiamando DbContext.SaveChanges () nel thread principale, ma l'aggiunta di entità al contesto viene eseguita nel thread in background, giusto?
Prokurors

1
Sì, aggiungi i dati all'interno dei thread; attendere che tutto finisca; e Salva modifiche nella discussione principale
Rafael AMS,

Anche se penso che in questo modo sia pericoloso e soggetto a errori, lo trovo molto interessante.
Principiante

1

Tutte le soluzioni scritte qui non aiutano perché quando si esegue SaveChanges (), le istruzioni di inserimento vengono inviate al database una per una, ecco come funziona Entity.

E se il tuo viaggio nel database e ritorno è di 50 ms, ad esempio, il tempo necessario per l'inserimento è il numero di record x 50 ms.

Devi usare BulkInsert, ecco il link: https://efbulkinsert.codeplex.com/

Ho usato il tempo di inserimento ridotto da 5-6 minuti a 10-12 secondi.



1

[NUOVA SOLUZIONE PER POSTGRESQL] Ehi, so che è un post piuttosto vecchio, ma di recente ho riscontrato un problema simile, ma stavamo usando Postgresql. Volevo usare un efficace bulkinsert, che si è rivelato piuttosto difficile. Non ho trovato alcuna libreria gratuita adeguata per farlo su questo DB. Ho trovato solo questo aiuto: https://bytefish.de/blog/postgresql_bulk_insert/ che è anche su Nuget. Ho scritto un piccolo mappatore, che ha mappato automaticamente le proprietà nel modo in cui 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;
        }

Lo uso nel modo seguente (avevo un'entità chiamata Undertaking):

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

Ho mostrato un esempio con la transazione, ma può anche essere fatto con una normale connessione recuperata dal contesto. enterprisesToAdd è un elenco di normali record di entità, che voglio inserire in blocco nel DB.

Questa soluzione, che ho dopo poche ore di ricerca e di prova, è come ci si potrebbe aspettare molto più velocemente e finalmente facile da usare e gratuita! Ti consiglio davvero di utilizzare questa soluzione, non solo per i motivi sopra menzionati, ma anche perché è l'unico con cui non ho avuto problemi con Postgresql stesso, molte altre soluzioni funzionano perfettamente, ad esempio con SqlServer.


0

Il segreto è inserire in una tabella di stadiazione vuota identica. Gli inserti si schiariscono rapidamente. Quindi esegui un singolo inserto da quello nella tabella principale principale. Quindi troncare la tabella di gestione temporanea pronta per il batch successivo.

vale a dire.

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

Utilizzando EF, aggiungi tutti i tuoi record a una tabella di gestione temporanea vuota. Quindi utilizzare SQL per inserire nella tabella principale (grande e lenta) in una singola istruzione SQL. Quindi svuota il tavolo di stadiazione. È un modo molto veloce per inserire molti dati in una tabella già grande.
Simon Hughes,

13
Quando hai detto di utilizzare EF, aggiungi i record alla tabella di gestione temporanea, hai effettivamente provato questo con EF? Poiché EF invia una chiamata separata al database con ogni inserimento, sospetto che vedrai lo stesso colpo perfetto che l'OP sta cercando di evitare. In che modo la tabella di gestione temporanea evita questo problema?
Jim Wooley,

-1

Ma, per più di (+4000) inserti, consiglio di usare la procedura memorizzata. allegato il tempo trascorso. L'ho inserito 11.788 righe in 20 "inserisci qui la descrizione dell'immagine

questo è il codice

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

-1

Utilizzare la procedura memorizzata che accetta i dati di input in formato XML per inserire i dati.

Dal tuo codice c # passa inserisci i dati come xml.

ad es. in c #, la sintassi sarebbe così:

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

-7

Utilizzare questa tecnica per aumentare la velocità di inserimento dei record in Entity Framework. Qui utilizzo una semplice procedura memorizzata per inserire i record. E per eseguire questa procedura memorizzata uso il metodo .FromSql () di Entity Framework che esegue Raw SQL.

Il codice della procedura memorizzata:

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

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

Quindi, scorrere tutti i 4000 record e aggiungere il codice Entity Framework che esegue l'archiviazione

la procedura continua ogni 100 ° ciclo.

Per questo creo una query di stringa per eseguire questa procedura, continuando ad aggiungervi ogni set di record.

Quindi controlla che il ciclo sia in esecuzione nei multipli di 100 e in quel caso eseguilo usando .FromSql().

Quindi per 4000 record devo eseguire la procedura solo per 4000/100 = 40 volte .

Controlla il codice qui sotto:

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

Questo potrebbe essere efficiente ma equivalente a NON utilizzare il framework di entità. La domanda del PO era come massimizzare l'efficienza nel contesto di Entity Framework
kall2sollies,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.