c# - works - what is entity framework




Il modo più veloce di inserimento in Entity Framework (17)

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

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


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

Sono disponibili alcune librerie di terze parti che supportano Bulk Insert:

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

Vedi: Entity Framework Bulk Inserisci libreria

Fai attenzione quando scegli una libreria di inserti di massa. Solo Entity Framework Extensions supporta tutti i tipi di associazioni ed ereditarietà ed è l'unico ancora supportato.

Dichiarazione di non responsabilità : sono il proprietario di Entity Framework Extensions

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

  • Salva modifiche in blocco
  • Inserto di massa
  • Elimina in blocco
  • Aggiornamento collettivo
  • Unisci in massa

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

Alla tua osservazione nei commenti alla tua domanda:

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

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

  • Chiama SaveChanges() una volta dopo TUTTI i record.
  • Chiama SaveChanges() dopo ad esempio 100 record.
  • Chiama SaveChanges() dopo 100 record ad esempio e disponi il contesto e creane 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" intorno a 100 o 1000). Migliora anche le prestazioni per disporre il contesto dopo SaveChanges e crearne uno nuovo. Ciò cancella il contesto da tutte le entrate, SaveChanges non lo fa, le entità sono ancora collegate al contesto nello stato Unchanged . È la dimensione crescente delle entità collegate nel contesto che rallenta l'inserimento passo dopo passo. Quindi, è utile cancellarlo dopo un po 'di tempo.

Ecco alcune misure per le mie 560.000 entità:

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

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


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

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


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

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

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

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


Ho studiato la risposta di Slauma (che è fantastica, grazie per l'idea dell'uomo), e ho ridotto le dimensioni del batch fino a quando non avrò raggiunto la velocità ottimale. Guardando i risultati di Slauma:

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

È visibile che c'è un aumento di velocità quando si passa da 1 a 10 e da 10 a 100, ma da 100 a 1000 la velocità di inserimento diminuisce di nuovo.

Quindi mi sono concentrato su cosa sta succedendo quando riduci le dimensioni del batch per valutare da qualche parte 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'optimum effettivo è intorno al valore di 30 per le dimensioni del lotto. È meno di entrambi i 10 e 100. Il problema è che non ho idea del perché sia ​​ottimale, né avrei potuto trovare alcuna spiegazione logica per questo.


Il modo più veloce sarebbe utilizzare l' estensione di inserimento di massa , che ho sviluppato.

Utilizza SqlBulkCopy e datareader personalizzato per ottenere prestazioni massime. Di conseguenza, è oltre 20 volte più veloce rispetto all'uso di inserti regolari o AddRange

l'utilizzo è estremamente semplice

context.BulkInsert(hugeAmountOfEntities);

Per quanto ne no BulkInsert in EntityFramework per aumentare le prestazioni degli enormi inserti.

In questo scenario puoi documentation in ADO.net per risolvere il tuo problema


Prova a utilizzare una stored procedure che otterrà un XML dei dati che desideri inserire.


Si consiglia di utilizzare System.Data.SqlClient.SqlBulkCopy per questo. Ecco la documentation e ovviamente ci sono molti tutorial online.

Scusate, so che stavate cercando una risposta semplice per convincere EF a fare ciò che volete, ma le operazioni collettive non sono proprio ciò per cui gli ORM sono destinati.


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 per il solo inserimento di massa questo è troppo.

Ecco un metodo di estensione molto semplice che ho creato. Lo uso su coppia con database prima (non testato prima con il codice, ma penso che funzioni allo stesso modo). Cambia YourEntities con 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))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            await bulkCopy.WriteToServerAsync(table);
        }
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            bulkCopy.WriteToServer(table);
        }
    }

    public string GetTableName(Type type)
    {
        var metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                .Single(e => objectItemCollection.GetClrType(e) == type);

        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
            .Single()
            .EntitySets
            .Single(s => s.ElementType.Name == entityType.Name);

        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                .Single()
                .EntitySetMappings
                .Single(s => s.EntitySet == entitySet);

        var table = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .StoreEntitySet;

        return (string)table.MetadataProperties["Table"].Value ?? table.Name;
    }
}

Puoi usarlo contro qualsiasi raccolta che erediti da IEnumerable , in questo modo:

await context.BulkInsertAllAsync(items);

Un'altra opzione è usare 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. Disclaimer: io sono l'autore di questa libreria e ogni opinione è di mia opinione.


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, AggiungiRange e SalvaChanges: non rileva le modifiche.

ValidateOnSaveEnabled = false;

Non rileva il tracker dei cambiamenti

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

Vorrei raccomandare questo articolo su come fare inserimenti di massa utilizzando EF.

Entity Framework e INSERT lenta alla rinfusa

Esplora queste aree e confronta la perfomance:

  1. EF predefinito (57 minuti per completare l'aggiunta di 30.000 record)
  2. Sostituzione con codice ADO.NET (25 secondi per quelli stessi 30.000)
  3. Context Bloat- Mantiene il grafico di contesto attivo piccolo usando un nuovo contesto per ciascuna unità di lavoro (gli stessi 30.000 inserimenti impiegano 33 secondi)
  4. Elenchi di grandi dimensioni - Disattiva AutoDetectChangesEnabled (porta il tempo a circa 20 secondi)
  5. Dosaggio (fino a 16 secondi)
  6. DbTable.AddRange () - (le prestazioni sono nell'intervallo 12)

Dispose() contesto Dispose() crea problemi se le entità che Add() dipendono da altre entità precaricate (es. Proprietà di navigazione) nel contesto

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

Ma invece di Dispose() il contesto e ricreare, semplicemente scollego 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 mostrandoli qui per mantenere pulito il codice


Utilizzare la stored procedure che prende i dati di input sotto forma di xml per inserire i dati.

Dal codice c #, inserisci i dati di inserimento come xml.

ad esempio in c #, la sintassi sarebbe come questa:

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


[NUOVA SOLUZIONE PER POSTGRESQL] Hey, so che è un post piuttosto vecchio, ma recentemente ho avuto problemi simili, ma stavamo usando Postgresql. Volevo usare il bulkinsert efficace, che risultò essere piuttosto difficile. Non ho trovato alcuna libreria libera appropriata per farlo su questo DB. Ho trovato solo questo helper: https://bytefish.de/blog/postgresql_bulk_insert/ che è anche su Nuget. Ho scritto un piccolo mapper, che auto mappava le proprietà come 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 come segue (avevo l'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 la normale connessione recuperata dal contesto. businessesToAdd è enumerabile di normali record di entità, che voglio bulkInsert nel DB.

Questa soluzione, alla quale ho preso dopo poche ore di ricerche e prove, è come potreste aspettarvi molto più veloce e, infine, facile da usare e gratis! Vi consiglio davvero di usare questa soluzione, non solo per le ragioni sopra menzionate, ma anche perché è l'unica con cui non ho avuto problemi con Postgresql stesso, molte altre soluzioni funzionano perfettamente, ad esempio con SqlServer.





entity-framework