Como faço para excluir várias linhas no Entity Framework (sem foreach)

305

Estou excluindo vários itens de uma tabela usando o Entity Framework. Não existe uma chave estrangeira / objeto pai, portanto não posso lidar com isso com o OnDeleteCascade.

Agora eu estou fazendo isso:

var widgets = context.Widgets
    .Where(w => w.WidgetId == widgetId);

foreach (Widget widget in widgets)
{
    context.Widgets.DeleteObject(widget);
}
context.SaveChanges();

Funciona, mas o foreach me incomoda. Estou usando EF4, mas não quero executar SQL. Eu só quero ter certeza de que não estou perdendo nada - isso é o melhor possível, certo? Eu posso abstraí-lo com um método de extensão ou auxiliar, mas em algum lugar ainda vamos fazer um foreach, certo?

Jon Galloway
fonte
1
Você pode revisitar a resposta aceita.
Eric J.
1
Se você quiser ficar performance talvez você pode querer verificar a minha resposta aqui stackoverflow.com/a/35033286/274589
Adi

Respostas:

49

Se você não deseja executar o SQL diretamente, chamando DeleteObject em um loop é o melhor que você pode fazer hoje.

No entanto, você pode executar o SQL e ainda torná-lo de propósito geral por meio de um método de extensão, usando a abordagem descrita aqui .

Embora essa resposta fosse para 3,5. Para a versão 4.0, eu provavelmente usaria a nova API ExecuteStoreCommand sob o capô, em vez de ir para o StoreConnection.

Alex James
fonte
ExecuteStoreCommand não é uma maneira adequada. DeleteAllSubmit está trabalhando em linq para sql, mas não na estrutura da entidade. Eu quero a mesma opção na estrutura da entidade.
Hiral
653

O EntityFramework 6 tornou isso um pouco mais fácil .RemoveRange().

Exemplo:

db.People.RemoveRange(db.People.Where(x => x.State == "CA"));
db.SaveChanges();
Kyle
fonte
31
É exatamente disso que precisamos ... Exceto quando o uso em uma faixa suficientemente grande, recebo uma exceção de falta de memória! Eu pensei que todo o objetivo do RemoveRange era passar o processamento para o banco de dados, mas aparentemente não.
precisa saber é o seguinte
isso é WAAAYYYY mais rápido do que definir o estado Excluído para todas as entidades!
Jerther
54
Certamente, essa resposta é mais fácil, mas, em termos de desempenho, pode não ser ótima. Por quê? o que exatamente esse doet é o mesmo que excluí-lo no loop foreach, ele primeiro busca todas as linhas e, em seguida, a exclusão é uma a uma, apenas o ganho é para salvar "DetectChanges será chamado uma vez antes de excluir qualquer entidade e não será chamado novamente". é o mesmo, tente usar a ferramenta para ver o sql gerado.
Anshul Nigam
6
Para uma gama bastante grande, tente algo como .Pegue (10000) e looping até RemoveRange (...) Count () == 0..
Eric J.
20
O problema é que o parâmetro de entrada RemoveRange é um IEnumerable; portanto, para executar a exclusão, enumera todas as entidades e execute 1 consulta DELETE por entidade.
bubi
74

isso é tão bom quanto ele ganha, certo? Eu posso abstraí-lo com um método de extensão ou auxiliar, mas em algum lugar ainda vamos fazer um foreach, certo?

Bem, sim, exceto que você pode transformá-lo em duas linhas:

context.Widgets.Where(w => w.WidgetId == widgetId)
               .ToList().ForEach(context.Widgets.DeleteObject);
context.SaveChanges();
Klaus Byskov Pedersen
fonte
76
Você está executando um ToList () que anula o objetivo. Como isso é diferente da solução original?
Lahsrah 31/05
3
Estou com problemas, pois só tenho o método Remove no objeto de contexto.
PNCT
2
Definitivamente, essa não é uma solução adequada quando se espera um milhão de linhas (ou mesmo algumas centenas). No entanto, se tivermos certeza de que haverá apenas algumas linhas, esta solução é pura e funciona perfeitamente bem. Sim, isso envolveria algumas viagens de ida e volta ao banco de dados, mas, na minha opinião, a abstração perdida envolvida na chamada de SQL supera diretamente os benefícios.
Yogster
O Entity Framework, como o nome sugere, funciona melhor com dados no nível da entidade. As operações de dados em massa são melhor tratadas por bons e antigos procs armazenados. Em termos de desempenho, são de longe as melhores opções e superam qualquer lógica EF que exija um loop.
Paceman
72
using (var context = new DatabaseEntities())
{
    context.ExecuteStoreCommand("DELETE FROM YOURTABLE WHERE CustomerID = {0}", customerId);
}
Vlad Bezden
fonte
Mas como você pode fazer isso com uma lista de IDs? Esta solução não lida com "listas" muito bem.
JesseNewman19
11
@ JesseNewman19 Se você já possui uma lista de IDs, use ae WHERE IN ({0})o segundo argumento deve ser String.Join(",", idList).
Langdon
@ Langdon que não funcionará, porque enviará o comando para o sql assim: WHERE IN ("1, 2, 3"). O banco de dados gera um erro porque você passou uma string em vez de uma lista de números inteiros.
precisa saber é o seguinte
Desejo gerar uma declaração como essa com o LINQ. A coisa mais próxima que encontrei foi uma lib. EntityFramework.Extended
Jaider
Se você estiver usando String.Join, pode ser necessário usar string.Formate passar a string SQL já formada para o comando. Desde que sua lista tenha apenas números inteiros, não há risco de ataque por injeção. Verifique esta pergunta: como posso passar uma matriz para um comando execute store?
18730 Andrew
50

Eu sei que é muito tarde, mas, caso alguém precise de uma solução simples, o interessante é que você também pode adicionar a cláusula where com ela:

public static void DeleteWhere<T>(this DbContext db, Expression<Func<T, bool>> filter) where T : class
{
    string selectSql = db.Set<T>().Where(filter).ToString();
    string fromWhere = selectSql.Substring(selectSql.IndexOf("FROM"));
    string deleteSql = "DELETE [Extent1] " + fromWhere;
    db.Database.ExecuteSqlCommand(deleteSql);
}

Nota: apenas testado com MSSQL2008.

Atualizar:

A solução acima não funcionará quando EF gerar instrução sql com parâmetros , então aqui está a atualização para EF5 :

public static void DeleteWhere<T>(this DbContext db, Expression<Func<T, bool>> filter) where T : class
{
    var query = db.Set<T>().Where(filter);

    string selectSql = query.ToString();
    string deleteSql = "DELETE [Extent1] " + selectSql.Substring(selectSql.IndexOf("FROM"));

    var internalQuery = query.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(field => field.Name == "_internalQuery").Select(field => field.GetValue(query)).First();
    var objectQuery = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(field => field.Name == "_objectQuery").Select(field => field.GetValue(internalQuery)).First() as ObjectQuery;
    var parameters = objectQuery.Parameters.Select(p => new SqlParameter(p.Name, p.Value)).ToArray();

    db.Database.ExecuteSqlCommand(deleteSql, parameters);
}

Requer um pouco de reflexão, mas funciona bem.

Thanh Nguyen
fonte
O que é o DbContext? Presumo que seu contexto de estrutura de entidade gerado automaticamente? Eu não tenho um método chamado Set <T>.
Rabino Stealth 29/08/2013
@ Stealth: Sim, é o seu contexto de dados EF, eu uso primeiro o código, mas o contexto gerado automaticamente deve ser o mesmo. Desculpe pela declaração incorreta, ela deve ser definida <T> () (minha empresa recupera o acesso à Internet, não consegui colar o código, tive que digitar manualmente, então ...), códigos atualizados :)
Thanh Nguyen
3
Esta é a única resposta que realmente responde à pergunta! Todas as outras respostas excluem cada item individual, um de cada vez, inacreditável.
Rocklan
Parece a resposta mais correta. Ele permite a exclusão de uma maneira muito genérica e descarrega adequadamente o trabalho no banco de dados e não no C #.
JesseNewman19
1
Para todos os programadores menos técnicos por aí, eu queria elaborar um pouco mais sobre como implementar essa solução excelente e genérica, porque isso me pouparia alguns minutos! Continua no próximo comentário ...
jdnew18 2/17
30

Para quem usa o EF5, a seguinte biblioteca de extensões pode ser usada: https://github.com/loresoft/EntityFramework.Extended

context.Widgets.Delete(w => w.WidgetId == widgetId);
Marcelo Mason
fonte
3
Tem problemas de desempenho em tabelas grandes, não utilizáveis ​​na minha situação.
Tomas
@Tomas, que tipo de desempenho emitido você notou? Quão grave foi a questão e quão grande era a mesa? Alguém mais pode confirmar isso?
Anestis Kivranoglou
Ele é muito rápido comparar com as alternativas lá fora
Jaider
Não consigo ver a Delete()função em minhas entidades no EF6.
dotNET
context.Widgets.Where(w => w.WidgetId == widgetId).Delete();é a maneira mais recente com EntityFramework.Extended
Peter Kerr
11

Ainda parece uma loucura ter que retirar qualquer coisa do servidor apenas para excluí-lo, mas pelo menos recuperar apenas os IDs é muito mais enxuto do que remover as entidades completas:

var ids = from w in context.Widgets where w.WidgetId == widgetId select w.Id;
context.Widgets.RemoveRange(from id in ids.AsEnumerable() select new Widget { Id = id });
Edward Brey
fonte
Cuidado: isso pode falhar na validação da entidade do Entity Framework porque seus Widgetobjetos stub têm apenas uma Idpropriedade inicializada . A maneira de contornar isso é usar context.Configuration.ValidateOnSaveEnabled = false(pelo menos no EF6). Isso desativa a validação própria do Entity Framework, mas ainda executa a validação própria do banco de dados, é claro.
Sammy S.
@SammyS. Eu não experimentei isso, então não posso falar com os detalhes, mas parece estranho que a EF se preocupe com a validação ao excluir a linha de qualquer maneira.
Edward Brey
Você está absolutamente correto. Eu confundi o deletecom uma solução semelhante paraupdate ing entidades sem carregá-los.
Sammy S.
10

EF 6.1

public void DeleteWhere<TEntity>(Expression<Func<TEntity, bool>> predicate = null) 
where TEntity : class
{
    var dbSet = context.Set<TEntity>();
    if (predicate != null)
        dbSet.RemoveRange(dbSet.Where(predicate));
    else
        dbSet.RemoveRange(dbSet);

    context.SaveChanges();
} 

Uso:

// Delete where condition is met.
DeleteWhere<MyEntity>(d => d.Name == "Something");

Or:

// delete all from entity
DeleteWhere<MyEntity>();
mnsr
fonte
7
É efetivamente o mesmo que db.People.RemoveRange (db.People.Where (x => x.State == "CA")); db.SaveChanges (); Portanto, nenhum ganho de desempenho.
ReinierDG
4

Para o EF 4.1,

var objectContext = (myEntities as IObjectContextAdapter).ObjectContext;
objectContext.ExecuteStoreCommand("delete from [myTable];");
Amit Pawar
fonte
1
Isso funciona, mas o objetivo principal do uso do Entity Framework é ter uma maneira orientada a objetos para interagir com o banco de dados. Isso está apenas executando diretamente a consulta SQL.
Arturo Torres Sánchez
4

Você pode usar as bibliotecas de extensões para fazer isso, como EntityFramework.Extended ou Z.EntityFramework.Plus.EF6, existem disponíveis para EF 5, 6 ou Core. Essas bibliotecas têm ótimo desempenho quando você precisa excluir ou atualizar e usa o LINQ. Exemplo para excluir ( fonte mais ):

ctx.Users.Where(x => x.LastLoginDate < DateTime.Now.AddYears(-2)) .Delete();

ou ( fonte estendida )

context.Users.Where(u => u.FirstName == "firstname") .Delete();

Eles usam instruções SQL nativas, portanto, o desempenho é ótimo.

UUHHIVS
fonte
Pague 600 $ + pelo gerador de operações sql em massa. Seriamente?
Nicolay.anykienko
@ nicolay.anykienko Quando eu o usei, esta biblioteca foi livre, existem outras operações em que você tem que pagar, né não eu não sei se você tem que pagar
UUHHIVS
3

A maneira mais rápida de excluir é usando um procedimento armazenado. Eu prefiro procedimentos armazenados em um projeto de banco de dados em vez de SQL dinâmico, porque as renomeações serão tratadas corretamente e terão erros de compilador. O SQL dinâmico pode se referir a tabelas que foram excluídas / renomeadas, causando erros em tempo de execução.

Neste exemplo, eu tenho duas tabelas List e ListItems. Eu preciso de uma maneira rápida de excluir todos os ListItems de uma determinada lista.

CREATE TABLE [act].[Lists]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY, 
    [Name] NVARCHAR(50) NOT NULL
)
GO
CREATE UNIQUE INDEX [IU_Name] ON [act].[Lists] ([Name])
GO
CREATE TABLE [act].[ListItems]
(
    [Id] INT NOT NULL IDENTITY, 
    [ListId] INT NOT NULL, 
    [Item] NVARCHAR(100) NOT NULL, 
    CONSTRAINT PK_ListItems_Id PRIMARY KEY NONCLUSTERED (Id),
    CONSTRAINT [FK_ListItems_Lists] FOREIGN KEY ([ListId]) REFERENCES [act].[Lists]([Id]) ON DELETE CASCADE
)
go
CREATE UNIQUE CLUSTERED INDEX IX_ListItems_Item 
ON [act].[ListItems] ([ListId], [Item]); 
GO

CREATE PROCEDURE [act].[DeleteAllItemsInList]
    @listId int
AS
    DELETE FROM act.ListItems where ListId = @listId
RETURN 0

Agora, a parte interessante de excluir os itens e atualizar a estrutura do Entity usando uma extensão.

public static class ListExtension
{
    public static void DeleteAllListItems(this List list, ActDbContext db)
    {
        if (list.Id > 0)
        {
            var listIdParameter = new SqlParameter("ListId", list.Id);
            db.Database.ExecuteSqlCommand("[act].[DeleteAllItemsInList] @ListId", listIdParameter);
        }
        foreach (var listItem in list.ListItems.ToList())
        {
            db.Entry(listItem).State = EntityState.Detached;
        }
    }
}

O código principal agora pode ser usado como

[TestMethod]
public void DeleteAllItemsInListAfterSavingToDatabase()
{
    using (var db = new ActDbContext())
    {
        var listName = "TestList";
        // Clean up
        var listInDb = db.Lists.Where(r => r.Name == listName).FirstOrDefault();
        if (listInDb != null)
        {
            db.Lists.Remove(listInDb);
            db.SaveChanges();
        }

        // Test
        var list = new List() { Name = listName };
        list.ListItems.Add(new ListItem() { Item = "Item 1" });
        list.ListItems.Add(new ListItem() { Item = "Item 2" });
        db.Lists.Add(list);
        db.SaveChanges();
        listInDb = db.Lists.Find(list.Id);
        Assert.AreEqual(2, list.ListItems.Count);
        list.DeleteAllListItems(db);
        db.SaveChanges();
        listInDb = db.Lists.Find(list.Id);
        Assert.AreEqual(0, list.ListItems.Count);
    }
}
Xavier John
fonte
Obrigado por um bom exemplo de como usar um Procedimento Armazenado e implementá-lo como uma extensão, com o código de Uso.
Glenn Garson
3

Se você deseja excluir todas as linhas de uma tabela, você pode executar o comando sql

using (var context = new DataDb())
{
     context.Database.ExecuteSqlCommand("TRUNCATE TABLE [TableName]");
}

TRUNCATE TABLE (Transact-SQL) Remove todas as linhas de uma tabela sem registrar as exclusões de linhas individuais. TRUNCATE TABLE é semelhante à instrução DELETE sem a cláusula WHERE; no entanto, TRUNCATE TABLE é mais rápido e usa menos recursos do sistema e do log de transações.

Amir
fonte
3
Você também deve mencionar que não pode executar truncate tableem tabelas que são referenciadas por uma restrição FOREIGN KEY. (Você pode truncar uma tabela que tenha uma chave estrangeira que faça referência a si mesma.). Documentação MSDN
banda larga
2

UUHHIVSé uma maneira muito elegante e rápida de excluir lotes, mas deve ser usada com cuidado:

  • geração automática de transação: suas consultas serão abrangidas por uma transação
  • independência de contexto do banco de dados: sua execução não tem nada a ver com context.SaveChanges()

Esses problemas podem ser contornados assumindo o controle da transação. O código a seguir ilustra como excluir em lotes e inserir em massa de maneira transacional:

var repo = DataAccess.EntityRepository;
var existingData = repo.All.Where(x => x.ParentId == parentId);  

TransactionScope scope = null;
try
{
    // this starts the outer transaction 
    using (scope = new TransactionScope(TransactionScopeOption.Required))
    {
        // this starts and commits an inner transaction
        existingData.Delete();

        // var toInsert = ... 

        // this relies on EntityFramework.BulkInsert library
        repo.BulkInsert(toInsert);

        // any other context changes can be performed

        // this starts and commit an inner transaction
        DataAccess.SaveChanges();

        // this commit the outer transaction
        scope.Complete();
    }
}
catch (Exception exc)
{
    // this also rollbacks any pending transactions
    scope?.Dispose();
}
Alexei
fonte
2

Entidade da estrutura da entidade

3,1 3,0 2,2 2,1 2,0 1,1 1,0

using (YourContext context = new YourContext ())
{
    var widgets = context.Widgets.Where(w => w.WidgetId == widgetId);
    context.Widgets.RemoveRange(widgets);
    context.SaveChanges();
}

Resumo :

Remove a coleção de entidades fornecida do contexto subjacente ao conjunto, com cada entidade sendo colocada no estado Excluído, para que seja excluído do banco de dados quando SaveChanges for chamado.

Observações :

Observe que se System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled estiver definido como true (que é o padrão), DetectChanges será chamado uma vez antes de excluir qualquer entidade e não será chamado novamente. Isso significa que, em algumas situações, o RemoveRange pode ter um desempenho significativamente melhor do que chamar Remover várias vezes. Observe que, se alguma entidade existir no contexto no estado Adicionado, esse método fará com que ela seja desanexada do contexto. Isso ocorre porque uma entidade Adicionado não existe no banco de dados, de modo que tentar excluí-la não faz sentido.

Nguyen Van Thanh
fonte
1

Você pode executar consultas sql diretamente da seguinte maneira:

    private int DeleteData()
{
    using (var ctx = new MyEntities(this.ConnectionString))
    {
        if (ctx != null)
        {

            //Delete command
            return ctx.ExecuteStoreCommand("DELETE FROM ALARM WHERE AlarmID > 100");

        }
    }
    return 0;
}

Para selecionar, podemos usar

using (var context = new MyContext()) 
{ 
    var blogs = context.MyTable.SqlQuery("SELECT * FROM dbo.MyTable").ToList(); 
}
Abhishek Sharma
fonte
Como a EF não suporta adequadamente o mapeamento das condições de exclusão, essa é provavelmente a sua melhor aposta para realizar o trabalho.
Tony O'Hagan
1

Você também pode usar o método DeleteAllOnSubmit () passando seus resultados em uma lista genérica em vez de em var. Dessa forma, seu foreach se reduz a uma linha de código:

List<Widgets> widgetList = context.Widgets
              .Where(w => w.WidgetId == widgetId).ToList<Widgets>();

context.Widgets.DeleteAllOnSubmit(widgetList);

context.SubmitChanges();

Provavelmente ainda usa um loop internamente.

Hugo Nava Kopp
fonte
3
Parece que você está entendendo mal o que varé isso.
freedomn-m
1

A resposta de Thanh funcionou melhor para mim. Excluiu todos os meus registros em uma única viagem de servidor. Eu lutei para realmente chamar o método de extensão, então pensei em compartilhar o meu (EF 6):

Adicionei o método de extensão a uma classe auxiliar no meu projeto MVC e alterei o nome para "RemoveWhere". Injeto um dbContext nos meus controladores, mas você também pode fazer um using.

// make a list of items to delete or just use conditionals against fields
var idsToFilter = dbContext.Products
    .Where(p => p.IsExpired)
    .Select(p => p.ProductId)
    .ToList();

// build the expression
Expression<Func<Product, bool>> deleteList = 
    (a) => idsToFilter.Contains(a.ProductId);

// Run the extension method (make sure you have `using namespace` at the top)
dbContext.RemoveWhere(deleteList);

Isso gerou uma única instrução de exclusão para o grupo.

Steve Greene
fonte
0

EF 6. =>

var assignmentAddedContent = dbHazirBot.tbl_AssignmentAddedContent.Where(a =>
a.HazirBot_CategoryAssignmentID == categoryAssignment.HazirBot_CategoryAssignmentID);
dbHazirBot.tbl_AssignmentAddedContent.RemoveRange(assignmentAddedContent);
dbHazirBot.SaveChanges();
Erçin Dedeoğlu
fonte
0

Melhor : in EF6 => .RemoveRange()

Exemplo:

db.Table.RemoveRange(db.Table.Where(x => Field == "Something"));
maXXis
fonte
14
Como isso é diferente da resposta de Kyle?
-1

Veja a resposta 'código favorito' que funciona

Aqui está como eu o usei:

     // Delete all rows from the WebLog table via the EF database context object
    // using a where clause that returns an IEnumerable typed list WebLog class 
    public IEnumerable<WebLog> DeleteAllWebLogEntries()
    {
        IEnumerable<WebLog> myEntities = context.WebLog.Where(e => e.WebLog_ID > 0);
        context.WebLog.RemoveRange(myEntities);
        context.SaveChanges();

        return myEntities;
    }
Brian Quinn
fonte
1
Como sua resposta difere da resposta user1308743 ?
Sergey Berezovskiy
Eu estava simplesmente compartilhando um exemplo de trabalho. Tudo o que posso fazer para retribuir a ajuda que recebo aqui.
Brian Quinn
-3

No EF 6.2, isso funciona perfeitamente, enviando a exclusão diretamente para o banco de dados sem primeiro carregar as entidades:

context.Widgets.Where(predicate).Delete();

Com um predicado fixo, é bastante direto:

context.Widgets.Where(w => w.WidgetId == widgetId).Delete();

E se você precisar de um predicado dinâmico, dê uma olhada no LINQKit (pacote Nuget disponível), algo assim funciona bem no meu caso:

Expression<Func<Widget, bool>> predicate = PredicateBuilder.New<Widget>(x => x.UserID == userID);
if (somePropertyValue != null)
{
    predicate = predicate.And(w => w.SomeProperty == somePropertyValue);
}
context.Widgets.Where(predicate).Delete();
Vladimir
fonte
1
Com o EF 6.2 bruto, isso não é possível. Talvez você esteja usando Z.EntityFramework.Plusou algo parecido? ( Entityframework.net/batch-delete )
Sammy S.
O primeiro é o EF 6.2 bruto e os trabalhos encontram. O segundo é, como mencionei, usando o LINQKit.
Vladimir
1
Hmm, não consigo encontrar esse método. Você poderia verificar em qual classe e em qual namespace esse método reside?
Sammy S.
Terceiro, que (o Delete()método é inerentemente inexistente).
Sum None