A maneira mais rápida de inserir no Entity Framework

681

Estou procurando a maneira mais rápida de inserir no Entity Framework.

Estou perguntando isso por causa do cenário em que você tem um TransactionScope ativo e a inserção é enorme (mais de 4000). Pode potencialmente durar mais de 10 minutos (tempo limite padrão das transações) e isso levará a uma transação incompleta.

Bongo Sharp
fonte
1
Como você está fazendo isso atualmente?
Dustin Laine
Criando o TransactionScope, instanciando o DBContext, Abrindo a conexão e em uma instrução for-each, fazendo as inserções e SavingChanges (para cada registro), OBSERVAÇÃO: TransactionScope e DBContext estão usando instruções, e eu estou fechando a conexão finalmente. bloco
Bongo Sharp
Outra resposta para referência: stackoverflow.com/questions/5798646/…
Ladislav Mrnka 9/11
2
A maneira mais rápida de inserir em um banco de dados SQL não envolve EF. AFAIK Seu BCP então TVP + Merge / insert.
StingyJack
1
Para quem ler comentários: A resposta mais aplicável e moderna está aqui.
quer

Respostas:

985

À sua observação nos comentários à sua pergunta:

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

Essa é a pior coisa que você pode fazer! A chamada SaveChanges()para cada registro diminui extremamente as inserções em massa. Eu faria alguns testes simples que provavelmente melhorarão o desempenho:

  • Ligue SaveChanges()uma vez após TODOS os registros.
  • Ligue SaveChanges()depois, por exemplo, 100 registros.
  • Ligue SaveChanges()depois, por exemplo, 100 registros e descarte o contexto e crie um novo.
  • Desativar detecção de alterações

Para inserções em massa, estou trabalhando e experimentando um padrão 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;
}

Eu tenho um programa de teste que insere 560.000 entidades (9 propriedades escalares, sem propriedades de navegação) no banco de dados. Com esse código, ele funciona em menos de 3 minutos.

Para o desempenho, é importante chamar SaveChanges()depois de "muitos" registros ("muitos" em torno de 100 ou 1000). Também melhora o desempenho para descartar o contexto após SaveChanges e criar um novo. Isso limpa o contexto de todas as entidades, SaveChangesnão faz isso, as entidades ainda estão anexadas ao contexto no estado Unchanged. É o tamanho crescente das entidades anexadas no contexto que retarda a inserção passo a passo. Portanto, é útil limpá-lo depois de algum tempo.

Aqui estão algumas medidas para minhas 560000 entidades:

  • commitCount = 1, recreateContext = false: muitas horas (esse é o seu procedimento atual)
  • commitCount = 100, recreateContext = false: mais de 20 minutos
  • commitCount = 1000, recreateContext = false: 242 seg
  • commitCount = 10000, recreateContext = false: 202 seg
  • commitCount = 100000, recrteContext = false: 199 seg
  • commitCount = 1000000, recreateContext = false: exceção de falta de memória
  • commitCount = 1, recreateContext = true: mais de 10 minutos
  • commitCount = 10, recrteContext = true: 241 seg
  • commitCount = 100, recreateContext = true: 164 seg
  • commitCount = 1000, recreateContext = true: 191 seg

O comportamento no primeiro teste acima é que o desempenho é muito não linear e diminui extremamente ao longo do tempo. ("Muitas horas" é uma estimativa, eu nunca terminei este teste, parei em 50.000 entidades após 20 minutos.) Esse comportamento não linear não é tão significativo em todos os outros testes.

Slauma
fonte
89
@ Bongo Sharp: Não se esqueça de configurar AutoDetectChangesEnabled = false;o DbContext. Ele também tem um grande efeito de desempenho adicional: stackoverflow.com/questions/5943394/…
Slauma
6
Sim, o problema é que estou usando o Entity Framework 4, e o AutoDetectChangesEnabled faz parte do 4.1. No entanto, fiz o teste de desempenho e tive RESULTADOS SURPREENDENTES, passou de 00:12:00 para 00:00:22 SavinChanges em cada entidade estava fazendo o olverload ... MUITO OBRIGADO pelo seu answare! isto é o que eu estava procurando
Bongo afiada
10
Obrigado pelo contexto.Configuração.AutoDetectChangesEnabled = false; dica, faz uma enorme diferença.
Douglaz
1
@ dahacker89: Você está usando a versão correta EF> = 4.1 e DbContext, NOT ObjectContext?
Slauma
3
@ dahacker89: sugiro que você crie uma pergunta separada para o seu problema, talvez com mais detalhes. Não sou capaz de descobrir aqui o que está errado.
Slauma 07/07
176

Essa combinação aumenta a velocidade suficientemente bem.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
Arkhivania
fonte
46
Não desative o ValidateOnSaveEnabled às cegas, pois pode depender desse comportamento e não o perceba até que seja tarde demais. Então, novamente, você pode estar executando a validação em outro lugar no código e ter a EF validada novamente é completamente desnecessário.
Jeremy Cook
1
No meu teste, a economia de 20.000 linhas caiu de 101 segundos para 88 segundos. Não muito e quais são as implicações.
AH.
27
@ JeremyCook Acho que o que você está tentando chegar é que essa resposta seria muito melhor se explicasse as possíveis implicações de alterar essas propriedades dos valores padrão (além da melhoria de desempenho). Concordo.
Pseudocoder
1
Isso funcionou para mim, embora se você estiver registros de atualização no contexto você vai precisar ligar para DetectChanges () explicitamente
hillstuk
2
Estes podem ser desativado e depois reativado com um try-finally bloco: msdn.microsoft.com/en-us/data/jj556205.aspx
yellavon
98

A maneira mais rápida seria usar a extensão de pastilhas em massa , que desenvolvi

nota: este é um produto comercial, gratuito

Ele usa SqlBulkCopy e datareader personalizado para obter desempenho máximo. Como resultado, é 20 vezes mais rápido que o uso de inserção regular ou AddRange EntityFramework.BulkInsert vs EF AddRange

o uso é extremamente simples

context.BulkInsert(hugeAmountOfEntities);
maxlego
fonte
10
Rápido, mas apenas a camada superior de uma hierarquia.
CAD bloke
65
Não é grátis.
Amir Saniyan
72
Os anúncios estão ficando mais inteligentes ... esse é um produto pago e muito caro para um freelancer. Esteja avisado!
JulioQc
35
USD600 por um ano de suporte e atualizações? Você está louco?
Camilo Terevinto
7
im não o proprietário do produto por mais tempo
maxlego
83

Você deve usar o System.Data.SqlClient.SqlBulkCopypara isso. Aqui está a documentação e, é claro, existem muitos tutoriais online.

Desculpe, eu sei que você estava procurando uma resposta simples para que a EF fizesse o que você deseja, mas as operações em massa não são realmente para o que os ORMs são destinados.

Adam Rackis
fonte
1
Eu encontrei o SqlBulkCopy algumas vezes enquanto pesquisava isso, mas parece ser mais orientado para inserções tabela a tabela, infelizmente, eu não esperava soluções fáceis, mas dicas de desempenho, como, por exemplo, o gerenciamento do estado do conexão manualmente, insted de deixar EF fazer isso por você
Bongo afiada
7
Eu usei o SqlBulkCopy para inserir grandes quantidades de dados diretamente do meu aplicativo. Você basicamente tem que criar uma DataTable, preenchê-lo, em seguida, passar que para BulkCopy. Existem algumas armadilhas que você estiver configurando o seu DataTable (a maioria dos quais eu tenha esquecido, infelizmente), mas deve funcionar muito bem
Adam Rackis
2
Fiz a prova do conceito e, como prometido, funciona muito rápido, mas uma das razões pelas quais estou usando o EF é porque a inserção de dados relacionais é mais fácil, por exemplo, se eu inserir uma entidade que já contém dados relacionais , ele também será inserido, você já entrou nesse cenário? Obrigado!
Bongo Sharp
2
Infelizmente, inserir uma rede de objetos em um DBMS não é realmente algo que o BulkCopy fará. Esse é o benefício de um ORM como o EF, o custo é que ele não será dimensionado para gerar centenas de gráficos de objetos semelhantes com eficiência.
Adam Rackis
2
SqlBulkCopy é definitivamente o caminho a percorrer, se você precisar de velocidade bruta ou se estiver executando novamente esta inserção. Eu inseri vários milhões de registros nele antes e é extremamente rápido. Dito isto, a menos que você precise executar novamente essa inserção, pode ser mais fácil usar o EF.
26412 Neil
49

Eu concordo com Adam Rackis. SqlBulkCopyé a maneira mais rápida de transferir registros em massa de uma fonte de dados para outra. Usei isso para copiar 20 mil registros e levou menos de 3 segundos. Veja o exemplo abaixo.

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
fonte
1
Tentei muitas das soluções fornecidas neste post e o SqlBulkCopy foi de longe o mais rápido. A EF pura levou 15 minutos, mas com uma mistura da solução e SqlBulkCopy eu consegui descer para 1,5 min! Isso foi com 2 milhões de registros! Sem qualquer otimização do índice de banco de dados.
jonas
Lista é mais fácil que DataTable. Existe um AsDataReader()método de extensão, explicado nesta resposta: stackoverflow.com/a/36817205/1507899
RJB
Mas sua única para cima Entidade não relacional
Zahid Mustafa
1
@ZahidMustafa: sim. Ele está executando BulkInsert, não Bulk-Analysis-And-Relation-Tracing-On-Graphs Graphs. Se você deseja cobrir relações, precisa analisar e determinar a ordem de inserção e, em seguida, inserir em massa os níveis individuais e, talvez, atualizar algumas teclas como necessário, e você obterá uma solução personalizada rápida. Ou você pode confiar na EF para fazer isso, sem trabalho do seu lado, mas mais lento no tempo de execução.
quetzalcoatl
23

Eu recomendaria este artigo sobre como fazer inserções em massa usando EF.

Entity Framework e INSERTs em massa lenta

Ele explora essas áreas e compara o desempenho:

  1. EF padrão (57 minutos para completar a adição de 30.000 registros)
  2. Substituindo pelo código ADO.NET (25 segundos para os mesmos 30.000)
  3. Inchaço do contexto - mantenha o gráfico de contexto ativo pequeno, usando um novo contexto para cada unidade de trabalho (as mesmas 30.000 inserções levam 33 segundos)
  4. Listas grandes - desative AutoDetectChangesEnabled (reduz o tempo para cerca de 20 segundos)
  5. Lote (até 16 segundos)
  6. DbTable.AddRange () - (o desempenho está no intervalo 12)
ShaTin
fonte
21

como nunca foi mencionado aqui, quero recomendar EFCore.BulkExtensions aqui

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
fonte
1
Eu apóio essa sugestão. Depois de tentar muitas soluções caseiras, reduzi a pastilha para 1 segundo em mais de 50 segundos. E é uma licença do MIT tão fácil de incorporar.
SouthShoreAK
está disponível para ef 6.x
Alok
este é apenas mais eficaz do que usar AddRange se é mais de 10 entidades
Jackal
5
10.000 inserções passaram de 9 minutos para 12 segundos. Isso merece mais atenção!
callisto
2
Se houver alguma maneira de alterar as respostas aceitas, essa deve ser a resposta aceita moderna agora. E eu desejo que a equipe da EF tenha fornecido isso imediatamente.
Tanveer Badar
18

Eu investiguei a resposta de Slauma (o que é incrível, obrigado pela ideia) e reduzi o tamanho do lote até atingir a velocidade ideal. Olhando para os resultados do Slauma:

  • commitCount = 1, recreateContext = true: mais de 10 minutos
  • commitCount = 10, recrteContext = true: 241 seg
  • commitCount = 100, recreateContext = true: 164 seg
  • commitCount = 1000, recreateContext = true: 191 seg

É visível que há aumento de velocidade ao passar de 1 para 10 e de 10 para 100, mas de 100 para 1000 a velocidade de inserção está caindo novamente.

Portanto, concentrei-me no que está acontecendo quando você reduz o tamanho do lote para um valor entre 10 e 100, e aqui estão meus resultados (estou usando diferentes conteúdos de linha, portanto, meus tempos são de valor diferente):

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

Com base nos meus resultados, o valor real ideal é de cerca de 30 para o tamanho do lote. É menor que 10 e 100. O problema é que não tenho idéia do porquê de 30 ser o ideal, nem poderia ter encontrado uma explicação lógica para isso.

Admir Tuzović
fonte
2
Achei o mesmo com Postrges e SQL puro (depende do SQL e não do EF) que 30 é o ideal.
Kamil Gareev
Minha experiência é que o melhor difere para diferentes velocidades de conexão e tamanho da linha. Para conexão rápida e pequenas filas, o ideal pode ser até 200 linhas.
jing
18

Como outras pessoas disseram, SqlBulkCopy é a maneira de fazer isso, se você deseja realmente um bom desempenho de pastilha.

É um pouco complicado de implementar, mas existem bibliotecas que podem ajudá-lo. Existem alguns por aí, mas vou descaradamente conectar minha própria biblioteca neste momento: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

O único código que você precisa é:

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

Então, quanto mais rápido é? É muito difícil dizer, porque depende de muitos fatores, desempenho do computador, rede, tamanho do objeto, etc. Os testes de desempenho sugeridos sugerem que 25k entidades podem ser inseridas a cerca de 10s da maneira padrão no host local, se você otimizar sua configuração EF como mencionado nas outras respostas. Com EFUtilities que leva cerca de 300ms. Ainda mais interessante é que salvei cerca de 3 milhões de entidades em menos de 15 segundos usando esse método, com média de cerca de 200 mil entidades por segundo.

O único problema é claro se você precisar inserir dados repetidos. Isso pode ser feito com eficiência no servidor sql usando o método acima, mas requer que você tenha uma estratégia de geração de ID que permita gerar IDs no código do aplicativo para o pai, para que você possa definir as chaves estrangeiras. Isso pode ser feito usando GUIDs ou algo como a geração de ID do HiLo.

Mikael Eliasson
fonte
Funciona bem. A sintaxe é um pouco detalhada, no entanto. Pense que seria melhor se EFBatchOperationtivesse um construtor para o qual você passa no DbContextlugar de todo método estático. Versões genéricas InsertAlle UpdateAllque automaticamente acham a coleção, semelhante a DbContext.Set<T>, também seriam boas.
kjbartel
Apenas um comentário rápido para dizer obrigado! Esse código me permitiu salvar 170k registros em 1,5 segundos! Sopra completamente qualquer outro método que tentei fora da água.
Tom Glenn
@Mikael Uma questão é lidar com campos de identidade. Você já tem uma maneira de ativar a inserção de identidade?
Joe Phillips
1
Em contraste com EntityFramework.BulkInsert, essa biblioteca permaneceu livre. +1
Rudey 12/10/19
14

Dispose()O contexto cria problemas se as entidades em que você Add()confia em outras entidades pré-carregadas (por exemplo, propriedades de navegação) no contexto

Uso conceitos semelhantes para manter meu contexto pequeno e obter o mesmo desempenho

Mas, em vez do Dispose()contexto e recriar, simplesmente desanexo as entidades que já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;
} }

envolva-o com try catch e, TrasactionScope()se necessário, não os mostre aqui para manter o código limpo

Stephen Ho
fonte
1
Isso atrasou a inserção (AddRange) usando o Entity Framework 6.0. A inserção de 20.000 linhas passou de 101 segundos para 118 segundos.
AH.
1
@ Stephen Ho: Eu também estou tentando evitar descartar meu contexto. Entendo que isso é mais lento do que recriar o contexto, mas quero saber se você achou isso mais rápido do que não recriar o contexto, mas com um conjunto commitCount.
Learner
@ Aluno: Eu acho que foi mais rápido do que recriar o contexto. Mas eu realmente não me lembro agora, porque eu mudei para usar SqlBulkCopy finalmente.
Stephen Ho
Acabei tendo que usar essa técnica porque, por algum motivo estranho, havia algum sobra de rastreamento ocorrendo na segunda passagem pelo loop while, mesmo que eu tivesse tudo envolto em uma instrução using e chamado Dispose () no DbContext . Quando eu adicionava ao contexto (na segunda passagem), a contagem do conjunto de contextos passava para 6 em vez de apenas um. Os outros itens que foram adicionados arbitrariamente já haviam sido inseridos na primeira passagem pelo loop while, para que a chamada para SaveChanges falhasse na segunda passagem (por razões óbvias).
Hallmanac
9

Eu sei que essa é uma pergunta muito antiga, mas um cara aqui disse que desenvolveu um método de extensão para usar a inserção em massa com EF e, quando verifiquei, descobri que a biblioteca custa US $ 599 hoje (para um desenvolvedor). Talvez faça sentido para toda a biblioteca, no entanto, para apenas a inserção em massa, isso é demais.

Aqui está um método de extensão muito simples que eu criei. Eu uso isso em par com o banco de dados primeiro (não testei com o código primeiro, mas acho que funciona da mesma forma). Mude YourEntitiescom o nome do seu 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;
    }
}

Você pode usá-lo em qualquer coleção que herda IEnumerable, assim:

await context.BulkInsertAllAsync(items);
Guilherme
fonte
por favor, complete seu código de exemplo. Onde está bulkCopy
Seabizkit
1
Já está aqui:await bulkCopy.WriteToServerAsync(table);
Guilherme
Talvez eu não tenha sido claro, em sua redação, você sugere que você fez uma extensão ... o que eu entendi como nenhuma lib de terceira parte, quando de fato, em ambos os métodos, usamos SqlBulkCopy lib. Isso depende inteiramente do SqlBulkCopy, quando eu perguntei de onde vem o bulkCopy, é uma biblioteca de extensões na qual você escreveu uma biblioteca de extensões. Apenas faria mais sentido dizer aqui é como eu usei o SqlBulkCopy lib.
Seabizkit
deve usar conn.OpenAsync na versão assíncrona
Robert
6

Tente usar um procedimento armazenado que obterá um XML dos dados que você deseja inserir.

Máxima
fonte
9
A transmissão de dados como XML não é necessária se você não desejar armazená-los como XML. No SQL 2008, você pode usar o parâmetro com valor de tabela.
Ladislav Mrnka
eu não esclarecer isso, mas eu preciso também apoiar SQL 2005
Bongo afiada
4

Eu fiz uma extensão genérica do exemplo de @Slauma acima;

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
fonte
4

Estou procurando a maneira mais rápida de inserir no Entity Framework

Existem algumas bibliotecas de terceiros compatíveis com o Bulk Insert disponíveis:

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

Consulte: Biblioteca de inserção em massa do Entity Framework

Tenha cuidado ao escolher uma biblioteca de inserção em massa. Somente o Entity Framework Extensions suporta todos os tipos de associações e heranças e é o único ainda suportado.


Isenção de responsabilidade : sou o proprietário do Entity Framework Extensions

Esta biblioteca permite que você execute todas as operações em massa necessárias para seus cenários:

  • Mudanças em massa
  • Inserção em massa
  • Excluir em massa
  • Atualização em massa
  • Mesclagem em massa

Exemplo

// 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
fonte
19
Esta é uma grande extensão, mas não é gratuita .
Okan Kocyigit
2
Essa resposta é muito boa e o EntityFramework.BulkInsert realiza uma inserção em massa de 15 mil linhas em 1,5 segundos, funciona muito bem para um processo interno como um Serviço do Windows.
Pastor Cortes
4
Sim, 600 $ para inserção em massa. Totalmente vale a pena.
eocron
1
@eocron Yeat, vale a pena se você usá-lo comercialmente. Não vejo nenhum problema com US $ 600 por algo que não preciso gastar horas construindo sozinho, o que me custará muito mais do que US $ 600. Sim, custa dinheiro, mas olhando para a minha taxa horária é dinheiro bem gasto!
Jordy van Eijk
3

Use 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
fonte
3

Uma das maneiras mais rápidas de salvar uma lista, você deve aplicar o seguinte código

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

AutoDetectChangesEnabled = false

Add, AddRange & SaveChanges: Não detecta alterações.

ValidateOnSaveEnabled = false;

Não detecta o rastreador de alterações

Você deve adicionar pepitas

Install-Package Z.EntityFramework.Extensions

Agora você pode usar o seguinte código

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();
Reza Jenabi
fonte
posso usar o seu código de amostra para atualização em massa?
AminGolmahalle 5/11
4
Biblioteca Z não é livre
SHADOW.NET
3

SqlBulkCopy é super rápido

Esta é a minha implementação:

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

[Atualização 2019] EF Core 3.1

Seguindo o que foi dito acima, a desativação do AutoDetectChangesEnabled no EF Core funcionou perfeitamente: o tempo de inserção foi dividido por 100 (de muitos minutos a alguns segundos, 10k registros com relacionamentos entre tabelas)

O código atualizado é:

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

Aqui está uma comparação de desempenho entre o uso do Entity Framework e o uso da classe SqlBulkCopy em um exemplo realista: Como inserir objetos complexos em massa no banco de dados do SQL Server

Como outros já enfatizaram, os ORMs não devem ser usados ​​em operações em massa. Eles oferecem flexibilidade, separação de preocupações e outros benefícios, mas as operações em massa (exceto a leitura em massa) não são uma delas.

Zoran Horvat
fonte
2

Outra opção é usar o SqlBulkTools disponível no Nuget. É muito fácil de usar e possui alguns recursos poderosos.

Exemplo:

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 a documentação para obter mais exemplos e uso avançado. Isenção de responsabilidade: sou o autor desta biblioteca e qualquer opinião é de minha opinião.

Greg R Taylor
fonte
2
Este projeto foi excluído do NuGet e do GitHub.
0xced
1

Como por meu conhecimento existe no BulkInsertem EntityFrameworkaumentar o desempenho dos enormes inserções.

Neste cenário você pode ir com SqlBulkCopy em ADO.netresolver o seu problema

anishMarokey
fonte
Eu estava dando uma olhada nessa classe, mas parece ser mais orientada para inserções tabela a tabela, não é?
Bongo Sharp
Não sei o que você quer dizer, ele tem uma sobrecarga WriteToServerque leva a DataTable.
Blindy
Não, você pode inserir objetos .Net para SQL também. O que você está procurando?
AnishMarokey
Uma maneira de inserir potencialmente milhares de registros no banco de dados dentro de um bloco de TransactionScope
Bongo afiada
você pode usar Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey
1

Você já tentou inserir através de um trabalhador ou tarefa em segundo plano?

No meu caso, estou inserindo 7760 registradores, distribuídos em 182 tabelas diferentes com relacionamentos de chave estrangeira (por NavigationProperties).

Sem a tarefa, demorou 2 minutos e meio. Dentro de uma tarefa ( Task.Factory.StartNew(...)), levou 15 segundos.

Estou apenas fazendo o SaveChanges()depois de adicionar todas as entidades ao contexto. (para garantir a integridade dos dados)

Rafael AMS
fonte
2
Tenho certeza de que o contexto não é seguro para threads. Você tem testes para garantir que todas as entidades foram salvas?
precisa
Eu sei que toda a estrutura da entidade não é um thread seguro, mas estou apenas adicionando os objetos ao contexto e salvando no final ... Está funcionando perfeitamente aqui.
Rafael AMS
Então, você está chamando DbContext.SaveChanges () no thread principal, mas a adição de entidades ao contexto é realizada no thread de segundo plano, certo?
Prokurors
1
Sim, adicione dados dentro dos threads; espere tudo terminar; e salvar as alterações no segmento principal
Rafael AMS
Embora eu ache que esse caminho é perigoso e propenso a erros, acho muito interessante.
Learner
1

Todas as soluções escritas aqui não ajudam, porque quando você faz SaveChanges (), as instruções de inserção são enviadas ao banco de dados uma a uma, é assim que a Entity funciona.

E se sua viagem ao banco de dados e ao retorno for de 50 ms, por exemplo, o tempo necessário para inserção será o número de registros x 50 ms.

Você precisa usar o BulkInsert, aqui está o link: https://efbulkinsert.codeplex.com/

O tempo de inserção foi reduzido de 5-6 minutos para 10-12 segundos usando-o.

Aleksa
fonte
1

[NOVA SOLUÇÃO PARA POSTGRESQL] Ei, eu sei que é um post bastante antigo, mas recentemente encontrei um problema semelhante, mas estávamos usando o Postgresql. Eu queria usar um bulkinsert eficaz, o que acabou sendo bastante difícil. Não encontrei nenhuma biblioteca gratuita adequada para fazer isso neste banco de dados. Eu encontrei apenas esse ajudante: https://bytefish.de/blog/postgresql_bulk_insert/, que também está no Nuget. Eu escrevi um pequeno mapeador, que mapeou automaticamente as propriedades da maneira como o 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;
        }

Eu o uso da seguinte maneira (eu tinha uma entidade chamada Empresa):

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

Eu mostrei um exemplo com transação, mas também pode ser feito com a conexão normal recuperada do contexto. compromkingsToAdd é enumerável de registros normais de entidade, que eu desejo inserir em massa no DB.

Esta solução, para a qual eu tenho depois de algumas horas de pesquisa e tentativa, é como você poderia esperar muito mais rápido e, finalmente, fácil de usar e grátis! Eu realmente aconselho você a usar esta solução, não apenas pelos motivos mencionados acima, mas também porque é a única com a qual não tive problemas com o próprio Postgresql, muitas outras soluções funcionam perfeitamente, por exemplo, com o SqlServer.

Michał Pilarek
fonte
0

O segredo é inserir em uma tabela de preparação em branco idêntica. As inserções são muito rápidas. Em seguida, execute uma única inserção disso na sua tabela principal principal. Em seguida, trunque a tabela de preparo pronta para o próximo lote.

ie

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
fonte
Usando EF, adicione todos os seus registros a uma tabela de preparação vazia. Em seguida, use SQL para inserir na tabela principal (grande e lenta) em uma única instrução SQL. Em seguida, esvazie sua mesa de preparação. É uma maneira muito rápida de inserir muitos dados em uma tabela já grande.
Simon Hughes
13
Quando você diz que usa EF, adicione os registros à tabela de preparação, você realmente tentou isso com EF? Como a EF emite uma chamada separada para o banco de dados com cada inserção, suspeito que você verá o mesmo desempenho que o OP está tentando evitar. Como a tabela de preparo evita esse problema?
11133 Jim Wooley
-1

Mas, para mais de (+4000) inserções, recomendo usar o procedimento armazenado. anexado o tempo decorrido. Eu inseri 11.788 linhas em 20 "insira a descrição da imagem aqui

é isso 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
fonte
-1

Use o procedimento armazenado que recebe dados de entrada em forma de xml para inserir dados.

A partir do seu código c #, insira os dados como xml.

por exemplo, em c #, a sintaxe seria assim:

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

Use esta técnica para aumentar a velocidade de inserção de registros no Entity Framework. Aqui eu uso um procedimento armazenado simples para inserir os registros. E para executar esse procedimento armazenado, uso o método .FromSql () do Entity Framework que executa o SQL bruto.

O código do procedimento armazenado:

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

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

Em seguida, faça um loop em todos os seus registros 4000 e adicione o código do Entity Framework que executa os

O procedimento é iniciado a cada 100º loop.

Para isso, crio uma consulta de string para executar este procedimento, continue anexando a ele todos os conjuntos de registros.

Em seguida, verifique se o loop está sendo executado nos múltiplos de 100 e, nesse caso, execute-o usando .FromSql().

Portanto, para 4000 registros, só preciso executar o procedimento apenas por 4000/100 = 40 vezes .

Verifique o código abaixo:

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
fonte
Isso pode ser eficiente, mas equivalente a NÃO usar a estrutura da entidade. A questão OP foi a forma de maximizar a eficiência no contexto do Entity Framework
kall2sollies