La forma más rápida de insertar en Entity Framework

681

Estoy buscando la forma más rápida de insertar en Entity Framework.

Pregunto esto debido al escenario en el que tiene un TransactionScope activo y la inserción es enorme (4000+). Potencialmente puede durar más de 10 minutos (tiempo de espera predeterminado de las transacciones), y esto conducirá a una transacción incompleta.

Bongo Sharp
fuente
1
¿Cómo lo estás haciendo actualmente?
Dustin Laine
Crear TransactionScope, crear instancias de DBContext, abrir la conexión y en una instrucción for-each haciendo las inserciones y SavingChanges (para cada registro), NOTA: TransactionScope y DBContext están utilizando instrucciones, y finalmente estoy cerrando la conexión bloque
Bongo Sharp
Otra respuesta para referencia: stackoverflow.com/questions/5798646/…
Ladislav Mrnka
2
La forma más rápida de insertar en una base de datos SQL no implica EF. AFAIK Su BCP y luego TVP + Fusionar / insertar.
StingyJack
1
Para aquellos que leerán comentarios: La respuesta más aplicable y moderna está aquí.
Tanveer Badar

Respuestas:

985

Para su comentario en los comentarios a su pregunta:

"... SavingChanges ( para cada registro ) ..."

¡Eso es lo peor que puedes hacer! La solicitud SaveChanges()de cada registro ralentiza las inserciones masivas extremadamente bajas. Haría algunas pruebas simples que muy probablemente mejorarán el rendimiento:

  • Llame SaveChanges()una vez después de TODOS los registros.
  • Llame SaveChanges()después, por ejemplo, 100 registros.
  • Llame SaveChanges()después de, por ejemplo, 100 registros y elimine el contexto y cree uno nuevo.
  • Deshabilitar detección de cambios

Para inserciones masivas, estoy trabajando y experimentando con un patrón como este:

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

Tengo un programa de prueba que inserta 560,000 entidades (9 propiedades escalares, sin propiedades de navegación) en la base de datos. Con este código funciona en menos de 3 minutos.

Para el rendimiento es importante llamar SaveChanges()después de "muchos" registros ("muchos" alrededor de 100 o 1000). También mejora el rendimiento para eliminar el contexto después de SaveChanges y crear uno nuevo. Esto borra el contexto de todas las entidades, SaveChangesno hace eso, las entidades todavía están unidas al contexto en estado Unchanged. Es el tamaño creciente de las entidades adjuntas en el contexto lo que ralentiza la inserción paso a paso. Por lo tanto, es útil borrarlo después de un tiempo.

Aquí hay algunas medidas para mis entidades 560000:

  • commitCount = 1, recreteContext = false: muchas horas (ese es su procedimiento actual)
  • commitCount = 100, recreteContext = false: más de 20 minutos
  • commitCount = 1000, recreteContext = false: 242 segundos
  • commitCount = 10000, recreateContext = false: 202 segundos
  • commitCount = 100000, recreateContext = false: 199 segundos
  • commitCount = 1000000, recreateContext = false: excepción de falta de memoria
  • commitCount = 1, recreteContext = true: más de 10 minutos
  • commitCount = 10, recreteContext = true: 241 segundos
  • commitCount = 100, recreteContext = true: 164 segundos
  • commitCount = 1000, recreateContext = true: 191 segundos

El comportamiento en la primera prueba anterior es que el rendimiento es muy no lineal y disminuye extremadamente con el tiempo. ("Muchas horas" es una estimación, nunca terminé esta prueba, me detuve en 50,000 entidades después de 20 minutos). Este comportamiento no lineal no es tan significativo en todas las otras pruebas.

Slauma
fuente
89
@Bongo Sharp: No te olvides de configurar AutoDetectChangesEnabled = false;el DbContext. También tiene un gran efecto de rendimiento adicional: stackoverflow.com/questions/5943394/…
Slauma
66
Sí, el problema es que estoy usando Entity Framework 4, y AutoDetectChangesEnabled es parte del 4.1, sin embargo, hice la prueba de rendimiento y tuve RESULTADOS INCREÍBLES, fue de 00:12:00 a 00:00:22 SavinChanges en cada entidad estaba haciendo la sobrecarga ... ¡GRACIAS por su answare! esto es lo que estaba buscando
Bongo Sharp
10
Gracias por el contexto.Configuración.AutoDetectChangesEnabled = false; consejo, hace una gran diferencia.
douglaz
1
@ dahacker89: ¿Está utilizando la versión correcta EF> = 4.1 y DbContextNO ObjectContext?
Slauma
3
@ dahacker89: le sugiero que cree una pregunta separada para su problema con quizás más detalles. No puedo entender aquí qué está mal.
Slauma
176

Esta combinación aumenta la velocidad lo suficientemente bien.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
arkhivania
fuente
46
No desactive ciegamente ValidateOnSaveEnabled, puede estar dependiendo de ese comportamiento, y no se dará cuenta hasta que sea demasiado tarde. Por otra parte, es posible que esté realizando la validación en otra parte del código y que EF valide una vez más es completamente innecesario.
Jeremy Cook
1
En mi prueba, el ahorro de 20,000 filas bajó de 101 segundos a 88 segundos. No mucho y cuáles son las implicaciones.
AH.
27
@JeremyCook Creo que lo que estás tratando de obtener es que esta respuesta sería mucho mejor si explicara las posibles implicaciones de cambiar estas propiedades de sus valores predeterminados (aparte de la mejora del rendimiento). Estoy de acuerdo.
pseudocoder
1
Esto funcionó para mí, aunque si está actualizando registros en el contexto, deberá llamar a DetectChanges () explícitamente
hillstuk
2
Estos se pueden deshabilitar y luego volver a habilitar con un bloque try-finally: msdn.microsoft.com/en-us/data/jj556205.aspx
yellavon
98

La forma más rápida sería usar la extensión de inserción masiva , que desarrollé

nota: este es un producto comercial, no gratuito

Utiliza SqlBulkCopy y un lector de datos personalizado para obtener el máximo rendimiento. Como resultado, es más de 20 veces más rápido que el uso de inserción regular o AddRange EntityFramework.BulkInsert vs EF AddRange

el uso es extremadamente simple

context.BulkInsert(hugeAmountOfEntities);
maxlego
fuente
10
Rápido pero solo lo hace la capa superior de una jerarquía.
CAD bloke
65
No es gratis
Amir Saniyan
72
Los anuncios se vuelven más inteligentes ... este es un producto pagado y muy costoso para un freelance. ¡Ten cuidado!
JulioQc
35
¿USD600 por 1 año de soporte y actualizaciones? ¿Estás loco?
Camilo Terevinto
77
Ya no soy el dueño del producto
maxlego
83

Deberías mirar el uso System.Data.SqlClient.SqlBulkCopyde esto. Aquí está la documentación y, por supuesto, hay muchos tutoriales en línea.

Lo siento, sé que estaba buscando una respuesta simple para que EF haga lo que desea, pero las operaciones masivas no son realmente para lo que están destinados los ORM.

Adam Rackis
fuente
1
Me he encontrado con SqlBulkCopy un par de veces mientras investigaba esto, pero parece estar más orientado a las inserciones de mesa a mesa, lamentablemente no esperaba soluciones fáciles, sino más bien consejos de rendimiento, como por ejemplo administrar el estado de la conexión manual, en lugar de dejar que EF lo haga por usted
Bongo Sharp
77
He usado SqlBulkCopy para insertar grandes cantidades de datos directamente desde mi aplicación. Es, básicamente, tiene que crear un DataTable, llenarlo, a continuación, pasar a que a bulkcopy. Hay algunas trampas mientras configura su DataTable (la mayoría de las cuales he olvidado, lamentablemente), pero debería funcionar bien
Adam Rackis
2
Hice la prueba de concepto y, como prometí, funciona muy rápido, pero una de las razones por las que estoy usando EF es porque la inserción de datos relacionales es más fácil, por ejemplo, si estoy insertando una entidad que ya contiene datos relacionales , también lo insertará, ¿alguna vez has entrado en este escenario? ¡Gracias!
Bongo Sharp
2
Desafortunadamente, insertar una red de objetos en un DBMS no es realmente algo que BulkCopy hará. Ese es el beneficio de un ORM como EF, el costo es que no se escalará para hacer cientos de gráficos de objetos similares de manera eficiente.
Adam Rackis
2
SqlBulkCopy es definitivamente el camino a seguir si necesita velocidad bruta o si volverá a ejecutar este inserto. He insertado varios millones de registros con él antes y es extremadamente rápido. Dicho esto, a menos que necesite volver a ejecutar este inserto, podría ser más fácil usar EF.
Neil
49

Estoy de acuerdo con Adam Rackis. SqlBulkCopyes la forma más rápida de transferir registros masivos de una fuente de datos a otra. Utilicé esto para copiar 20K registros y me tomó menos de 3 segundos. Echa un vistazo al siguiente ejemplo.

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
fuente
1
Probé muchas de las soluciones proporcionadas en esta publicación y SqlBulkCopy fue, con mucho, la más rápida. EF puro tardó 15 minutos, pero con una mezcla de la solución y SqlBulkCopy pude llegar a 1.5 minutos. ¡Esto fue con 2 millones de discos! Sin ninguna optimización de índice de base de datos.
jonas
La lista es más fácil que DataTable. Hay un AsDataReader()método de extensión, explicado en esta respuesta: stackoverflow.com/a/36817205/1507899
RJB
Pero es solo para la entidad superior, no la relacional
Zahid Mustafa
1
@ZahidMustafa: sí. Está haciendo BulkInsert, no Bulk-Analysis-And-Relation-Tracing-On-Object-Graphs ... si desea cubrir relaciones, debe analizar y determinar el orden de inserción y luego insertar en masa niveles individuales y quizás actualizar algunas claves como necesario, y obtendrá una solución personalizada rápida y personalizada. O bien, puede confiar en EF para hacer eso, sin trabajo de su lado, pero más lento en tiempo de ejecución.
quetzalcoatl
23

Recomendaría este artículo sobre cómo hacer inserciones masivas usando EF.

Entity Framework e INSERTs masivos lentos

Explora estas áreas y compara el rendimiento:

  1. EF predeterminado (57 minutos para completar la adición de 30,000 registros)
  2. Reemplazar con el código ADO.NET (25 segundos para esos mismos 30,000)
  3. Contexto hinchado: mantenga pequeño el gráfico de contexto activo mediante el uso de un nuevo contexto para cada unidad de trabajo (los mismos 30,000 insertos toman 33 segundos)
  4. Listas grandes: desactive AutoDetectChangesEnabled (reduce el tiempo a unos 20 segundos)
  5. Lote (hasta 16 segundos)
  6. DbTable.AddRange () - (el rendimiento está en el rango 12)
ShaTin
fuente
21

como nunca se mencionó aquí, quiero recomendar EFCore.BulkExtensions aquí

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
fuente
1
Secundo esta sugerencia. Después de probar muchas soluciones caseras, esto redujo mi inserción a 1 segundo desde más de 50 segundos. Y, es una licencia MIT tan fácil de incorporar.
SouthShoreAK
Está disponible para ef 6.x
Alok
esto es solo más eficaz que usar AddRange si tiene más de 10 entidades
Jackal
55
10 000 insertos pasaron de 9 minutos a 12 segundos. ¡Esto merece más atención!
callisto
2
Si hay alguna forma de cambiar las respuestas aceptadas, esta debería ser la respuesta moderna aceptada ahora. Y deseo que el equipo de EF haya proporcionado esto de manera inmediata.
Tanveer Badar
18

Investigué la respuesta de Slauma (que es increíble, gracias por la idea, hombre), y reduje el tamaño del lote hasta alcanzar la velocidad óptima. Mirando los resultados de Slauma:

  • commitCount = 1, recreteContext = true: más de 10 minutos
  • commitCount = 10, recreteContext = true: 241 segundos
  • commitCount = 100, recreteContext = true: 164 segundos
  • commitCount = 1000, recreateContext = true: 191 segundos

Es visible que hay un aumento de velocidad cuando se mueve de 1 a 10, y de 10 a 100, pero de 100 a 1000 la velocidad de inserción vuelve a caer.

Por lo tanto, me he centrado en lo que sucede cuando reduce el tamaño del lote a un valor entre 10 y 100, y aquí están mis resultados (estoy usando diferentes contenidos de fila, por lo que mis tiempos son de diferente valor):

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

Según mis resultados, el valor óptimo real es de alrededor de 30 para el tamaño del lote. Es menor que 10 y 100. El problema es que no tengo idea de por qué es 30 óptimo, ni podría haber encontrado ninguna explicación lógica para ello.

Admir Tuzović
fuente
2
Encontré lo mismo con Postrges y SQL puro (depende de SQL, no de EF) que 30 es óptimo.
Kamil Gareev
Mi experiencia es que el óptimo difiere para diferentes velocidades de conexión y tamaños de fila. Para una conexión rápida y filas pequeñas, las óptimas pueden ser incluso> 200 filas.
Jing
18

Como otras personas han dicho, SqlBulkCopy es la forma de hacerlo si desea un rendimiento de inserción realmente bueno.

Implementarlo es un poco engorroso, pero hay bibliotecas que pueden ayudarlo. Hay algunos por ahí, pero descaradamente conectaré mi propia biblioteca esta vez: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

El único código que necesitarías es:

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

Entonces, ¿cuánto más rápido es? Muy difícil de decir porque depende de muchos factores, el rendimiento de la computadora, la red, el tamaño del objeto, etc. Las pruebas de rendimiento que he realizado sugieren que se pueden insertar 25k entidades en aproximadamente 10 segundos de la manera estándar en localhost SI optimizas tu configuración de EF como mencionado en las otras respuestas. Con EFUtilities que dura unos 300 ms. Aún más interesante es que he ahorrado alrededor de 3 millones de entidades en menos de 15 segundos usando este método, promediando alrededor de 200k entidades por segundo.

El único problema es, por supuesto, si necesita insertar datos relegados. Esto se puede hacer de manera eficiente en el servidor SQL usando el método anterior, pero requiere que tenga una estrategia de generación de Id que le permita generar identificadores en el código de la aplicación para el padre para que pueda configurar las claves externas. Esto se puede hacer usando GUID o algo así como la generación de id de HiLo.

Mikael Eliasson
fuente
Funciona bien. Sin embargo, la sintaxis es un poco detallada. Piense que sería mejor si EFBatchOperationtuviera un constructor al que le pase en DbContextlugar de pasar a cada método estático. Las versiones genéricas de InsertAlly UpdateAllque encuentran automáticamente la colección, similar a DbContext.Set<T>, también serían buenas.
kjbartel
¡Solo un comentario rápido para decir gracias! ¡Este código me permitió guardar 170k registros en 1.5 segundos! Sopla completamente cualquier otro método que he probado fuera del agua.
Tom Glenn
@Mikael Un problema está relacionado con los campos de identidad. ¿Ya tiene una manera de habilitar la inserción de identidad?
Joe Phillips
1
En contraste con EntityFramework.BulkInsert, esta biblioteca permaneció libre. +1
Rudey
14

Dispose()el contexto crea problemas si las entidades en las que Add()confía en otras entidades precargadas (por ejemplo, propiedades de navegación) en el contexto

Utilizo un concepto similar para mantener mi contexto pequeño para lograr el mismo rendimiento

Pero en lugar del Dispose()contexto y recrear, simplemente separo las entidades que yaSaveChanges()

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

envuélvelo con try catch y TrasactionScope()si lo necesitas, no los muestres aquí para mantener el código limpio

Stephen Ho
fuente
1
Eso ralentizó la inserción (AddRange) usando Entity Framework 6.0. Insertar 20,000 filas aumentó de aproximadamente 101 segundos a 118 segundos.
AH.
1
@Stephen Ho: También estoy tratando de evitar deshacerme de mi contexto. Puedo entender que esto es más lento que recrear el contexto, pero quiero saber si lo encontraste lo suficientemente rápido como para no recrear el contexto pero con un conjunto commitCount.
Estudiante
@Learner: Creo que fue más rápido que recrear el contexto. Pero realmente no recuerdo ahora porque cambié para usar SqlBulkCopy por fin.
Stephen Ho
Terminé teniendo que usar esta técnica porque, por alguna extraña razón, había algo de seguimiento restante en la segunda pasada a través del ciclo while, a pesar de que tenía todo envuelto en una declaración de uso e incluso llamado Dispose () en el DbContext . Cuando agregaría al contexto (en la segunda pasada), el recuento del conjunto de contexto saltaría a 6 en lugar de solo uno. Los otros elementos que se agregaron arbitrariamente ya se habían insertado en la primera pasada a través del ciclo while, por lo que la llamada a SaveChanges fallaría en la segunda pasada (por razones obvias).
Hallmanac
9

Sé que esta es una pregunta muy antigua, pero un tipo aquí dijo que desarrolló un método de extensión para usar inserción masiva con EF, y cuando lo comprobé, descubrí que la biblioteca cuesta $ 599 hoy (para un desarrollador). Tal vez tenga sentido para toda la biblioteca, sin embargo, solo para la inserción masiva, esto es demasiado.

Aquí hay un método de extensión muy simple que hice. Lo uso primero en pareja con la base de datos (no lo pruebo primero con el código, pero creo que funciona igual). Cambiar YourEntitiescon el nombre de su contexto:

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

Puede usar eso contra cualquier colección que herede de IEnumerable, así:

await context.BulkInsertAllAsync(items);
Guilherme
fuente
por favor complete su código de ejemplo. ¿Dónde está BulkCopy
Seabizkit
1
Ya está aquí:await bulkCopy.WriteToServerAsync(table);
Guilherme
Tal vez no estaba claro, en su escrito, sugiere que hizo una extensión ... lo que entendí que significaba que no se necesitaba una tercera parte de lib, cuando de hecho en ambos métodos usa SqlBulkCopy lib. Esto se basa completamente en SqlBulkCopy, cuando por qué pregunté de dónde viene bulkCopy, es una extensión lib de la que escribiste una extensión lib. Simplemente tendría más sentido decir aquí cómo utilicé SqlBulkCopy lib.
Seabizkit
debería usar conn.OpenAsync en versión asíncrona
Robert
6

Intente utilizar un Procedimiento almacenado que obtenga un XML de los datos que desea insertar.

Máxima
fuente
99
No es necesario pasar datos como XML si no desea almacenarlos como XML. En SQL 2008 puede usar el parámetro con valores de tabla.
Ladislav Mrnka
No aclaré esto, pero también necesito admitir SQL 2005
Bongo Sharp
4

He hecho una extensión genérica del ejemplo anterior de @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;
    }
}

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

Estoy buscando la forma más rápida de insertar en Entity Framework

Hay algunas bibliotecas de terceros que admiten Bulk Insert disponibles:

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

Ver: biblioteca de inserción masiva de Entity Framework

Tenga cuidado al elegir una biblioteca de inserción masiva. Solo Entity Framework Extensions admite todo tipo de asociaciones y herencias y es el único que aún se admite.


Descargo de responsabilidad : soy el propietario de Entity Framework Extensions

Esta biblioteca le permite realizar todas las operaciones masivas que necesita para sus escenarios:

  • Guardar cambios a granel
  • Inserto a granel
  • Eliminar a granel
  • Actualización masiva
  • Fusión masiva

Ejemplo

// 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
fuente
19
Esta es una gran extensión, pero no es gratuita .
Okan Kocyigit
2
Esta respuesta es bastante buena y EntityFramework.BulkInsert realiza una inserción masiva de 15K filas en 1.5 segundos, funciona bastante bien para un proceso interno como un Servicio de Windows.
Pastor Cortes
44
Sí, 600 $ por inserto a granel. Vale la pena.
eocron
1
@eocron Yeat vale la pena si lo usa comercialmente. No veo ningún problema con $ 600 por algo en lo que no tengo que pasar horas para construirlo yo mismo, lo que me costará mucho más de $ 600. Sí, cuesta dinero, pero mirando mi tarifa por hora, ¡es dinero bien gastado!
Jordy van Eijk
3

Uso 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
fuente
3

Una de las formas más rápidas de guardar una lista debe aplicar el siguiente código

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

AutoDetectChangesEnabled = false

Agregar, agregar rango y guardar cambios: no detecta cambios.

ValidateOnSaveEnabled = false;

No detecta el rastreador de cambios

Debes agregar nuget

Install-Package Z.EntityFramework.Extensions

Ahora puedes usar el siguiente código

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();
Reza Jenabi
fuente
¿Puedo usar su código de muestra para la actualización masiva?
AminGolmahalle
44
La biblioteca Z no es gratuita
SHADOW.NET
3

SqlBulkCopy es súper rápido

Esta es mi implementación:

// 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
fuente
3

[Actualización 2019] EF Core 3.1

Siguiendo lo que se dijo anteriormente, deshabilitar AutoDetectChangesEnabled en EF Core funcionó perfectamente: el tiempo de inserción se dividió entre 100 (de muchos minutos a unos segundos, 10k registros con relaciones de tablas cruzadas)

El código actualizado es:

  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
fuente
2

Aquí hay una comparación de rendimiento entre el uso de Entity Framework y el uso de la clase SqlBulkCopy en un ejemplo realista: Cómo insertar en masa objetos complejos en la base de datos de SQL Server

Como otros ya enfatizaron, los ORM no están destinados a ser utilizados en operaciones masivas. Ofrecen flexibilidad, separación de preocupaciones y otros beneficios, pero las operaciones masivas (excepto la lectura masiva) no son una de ellas.

Zoran Horvat
fuente
2

Otra opción es usar SqlBulkTools disponible de Nuget. Es muy fácil de usar y tiene algunas características poderosas.

Ejemplo:

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

Consulte la documentación para obtener más ejemplos y uso avanzado. Descargo de responsabilidad: soy el autor de esta biblioteca y cualquier opinión es de mi propia opinión.

Greg R Taylor
fuente
2
Este proyecto ha sido eliminado tanto de NuGet como de GitHub.
0xced
1

Según mi conocimiento no es no BulkInserten EntityFrameworkaumentar el rendimiento de las enormes inserciones.

En este escenario se puede ir con SqlBulkCopy en ADO.netresolver su problema

anishMarokey
fuente
Estaba mirando esa clase, pero parece estar más orientada a las inserciones de tabla a tabla, ¿no?
Bongo Sharp
No estoy seguro de lo que quieres decir, tiene una sobrecarga WriteToServerque requiere un DataTable.
Blindy
no, también puede insertar desde objetos .Net a SQL. ¿Qué está buscando?
anishMarokey
Una forma de insertar potencialmente miles de registros en la base de datos dentro de un bloque TransactionScope
Bongo Sharp
puede usar .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey
1

¿Alguna vez ha tratado de insertar a través de un trabajador o tarea en segundo plano?

En mi caso, estoy insertando 7760 registros, distribuidos en 182 tablas diferentes con relaciones de clave externa (por NavigationProperties).

Sin la tarea, tomó 2 minutos y medio. Dentro de una tarea ( Task.Factory.StartNew(...)), tardó 15 segundos.

Solo estoy haciendo lo siguiente SaveChanges()después de agregar todas las entidades al contexto. (para garantizar la integridad de los datos)

Rafael AMS
fuente
2
Estoy bastante seguro de que el contexto no es seguro para subprocesos. ¿Tiene pruebas para asegurarse de que se guardaron todas las entidades?
Danny Varod
Sé que todo el marco de la entidad no es seguro para los hilos, pero solo estoy agregando los objetos al contexto y guardando al final ... Funciona perfectamente aquí.
Rafael AMS
Entonces, estás llamando a DbContext.SaveChanges () en el hilo principal, pero agregar entidades al contexto se realiza en un hilo de fondo, ¿verdad?
Prokurors
1
Sí, agregue datos dentro de los hilos; espera a que todos terminen; y guardar cambios en el hilo principal
Rafael AMS
Aunque creo que este camino es peligroso y propenso a errores, lo encuentro muy interesante.
Estudiante
1

Todas las soluciones escritas aquí no ayudan porque cuando hace SaveChanges (), las instrucciones de inserción se envían a la base de datos una por una, así es como funciona la entidad.

Y si su viaje a la base de datos y de regreso es de 50 ms, por ejemplo, el tiempo necesario para la inserción es el número de registros x 50 ms.

Tienes que usar BulkInsert, aquí está el enlace: https://efbulkinsert.codeplex.com/

El tiempo de inserción se redujo de 5-6 minutos a 10-12 segundos al usarlo.

Aleksa
fuente
1

[NUEVA SOLUCIÓN PARA POSTGRESQL] Hola, sé que es una publicación bastante antigua, pero recientemente me he encontrado con un problema similar, pero estábamos usando Postgresql. Quería usar Bulkinsert efectivo, lo que resultó ser bastante difícil. No he encontrado ninguna biblioteca gratuita adecuada para hacerlo en este DB. Solo he encontrado este ayudante: https://bytefish.de/blog/postgresql_bulk_insert/ que también está en Nuget. He escrito un pequeño mapeador, que asigna automáticamente propiedades de la forma en que 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 de la siguiente manera (tenía una entidad llamada Undertaking):

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

Mostré un ejemplo con la transacción, pero también se puede hacer con una conexión normal recuperada del contexto. enterprisesToAdd es enumerable de registros de entidades normales, que quiero insertar a granel en DB.

¡Esta solución, que obtuve después de algunas horas de investigación y prueba, es como podría esperarse mucho más rápido y finalmente fácil de usar y gratis! Realmente te recomiendo que uses esta solución, no solo por las razones mencionadas anteriormente, sino también porque es la única con la que no tuve problemas con Postgresql, muchas otras soluciones funcionan perfectamente, por ejemplo con SqlServer.

Michał Pilarek
fuente
0

El secreto es insertar en una tabla de preparación en blanco idéntica. Los insertos se aclaran rápidamente. Luego, ejecute un único inserto desde ese punto en su tabla grande principal. Luego truncar la tabla de preparación lista para el próximo lote.

es decir.

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
fuente
Con EF, agregue todos sus registros a una tabla de etapas vacía. Luego use SQL para insertar en la tabla principal (grande y lenta) en una sola instrucción SQL. Luego vacia tu mesa de ensayo. Es una forma muy rápida de insertar muchos datos en una tabla ya grande.
Simon Hughes
13
Cuando dice que usando EF, agregue los registros a la tabla de etapas, ¿realmente intentó esto con EF? Como EF emite una llamada por separado a la base de datos con cada inserción, sospecho que verá el mismo golpe de rendimiento que el OP está tratando de evitar. ¿Cómo evita la tabla de etapas este problema?
Jim Wooley
-1

Pero, para más de (+4000) inserciones, recomiendo usar el procedimiento almacenado. adjunto el tiempo transcurrido. Lo inserté 11.788 filas en 20 "ingrese la descripción de la imagen aquí

eso es código

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

Utilice el procedimiento almacenado que toma datos de entrada en forma de xml para insertar datos.

Desde su pase de código C # inserte datos como xml

Por ejemplo, en C #, la sintaxis sería así:

object id_application = db.ExecuteScalar("procSaveApplication", xml)
arun tiwari
fuente
-7

Use esta técnica para aumentar la velocidad de inserción de registros en Entity Framework. Aquí utilizo un procedimiento almacenado simple para insertar los registros. Y para ejecutar este procedimiento almacenado, uso el método .FromSql () de Entity Framework que ejecuta SQL sin formato.

El código de procedimiento almacenado:

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

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

Luego, recorra todos sus 4000 registros y agregue el código de Entity Framework que ejecuta el almacenado

El procedimiento se realiza cada 100 ciclos.

Para esto creo una consulta de cadena para ejecutar este procedimiento, continúo agregando cada conjunto de registros.

Luego verifique que el ciclo se esté ejecutando en múltiplos de 100 y, en ese caso, ejecútelo usando .FromSql().

Entonces, para 4000 registros solo tengo que ejecutar el procedimiento solo 4000/100 = 40 veces .

Verifique el siguiente código:

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
fuente
Esto podría ser eficiente pero equivalente a NO usar el marco de la entidad. La pregunta OP fue cómo maximizar la eficiencia en el contexto de Entity Framework
kall2sollies